From 852abddd113efd6961f378f374686fc97dd4fb1e Mon Sep 17 00:00:00 2001 From: Bishoy-at-pieces Date: Mon, 10 Jun 2024 19:09:39 +0300 Subject: [PATCH 01/30] add ask stream websocket --- copilot/ask_websocket.py | 19 +++++++++++++++++++ settings.py | 2 ++ 2 files changed, 21 insertions(+) create mode 100644 copilot/ask_websocket.py diff --git a/copilot/ask_websocket.py b/copilot/ask_websocket.py new file mode 100644 index 0000000..2a6b8c1 --- /dev/null +++ b/copilot/ask_websocket.py @@ -0,0 +1,19 @@ +from pieces_os_client import QGPTStreamOutput +import threading + +from ..settings import PiecesSettings +from ..base_websocket import BaseWebsocket + +class AskStreamWS(BaseWebsocket): + def __new__(cls,*args,**kwargs): + if not hasattr(cls, 'instance'): + cls.instance = super(AssetsIdentifiersWS, cls).__new__(cls) + return cls.instance + + @property + def url(self): + return PiecesSettings.ASK_STREAM_WS_URL + + def on_message(self,ws, message): + self.on_message_callback(QGPTStreamOutput.from_json(message)) + diff --git a/settings.py b/settings.py index 989090f..ccda868 100644 --- a/settings.py +++ b/settings.py @@ -71,6 +71,8 @@ def host_init(cls): cls.AUTH_WS_URL = ws_base_url + "/user/stream" + cls.ASK_STREAM_WS_URL = ws_base_url + "/qgpt/stream" + configuration = pos_client.Configuration(host=cls.host) cls.api_client = pos_client.ApiClient(configuration) From bf5e3809c396eb26d1e45a7f7901b8afc963b57d Mon Sep 17 00:00:00 2001 From: Bishoy-at-pieces Date: Mon, 10 Jun 2024 19:53:50 +0300 Subject: [PATCH 02/30] add the websocket ask command --- copilot/__init__.py | 2 ++ copilot/ask_command.py | 10 ++++++++++ copilot/ask_websocket.py | 3 +-- main.py | 4 ++++ 4 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 copilot/__init__.py create mode 100644 copilot/ask_command.py diff --git a/copilot/__init__.py b/copilot/__init__.py new file mode 100644 index 0000000..a5620cc --- /dev/null +++ b/copilot/__init__.py @@ -0,0 +1,2 @@ +from .ask_websocket import AskStreamWS +from .ask_command import AskStreamCommand \ No newline at end of file diff --git a/copilot/ask_command.py b/copilot/ask_command.py new file mode 100644 index 0000000..eb07909 --- /dev/null +++ b/copilot/ask_command.py @@ -0,0 +1,10 @@ +from pieces_os_client import QGPTQuestionOutput +import sublime_plugin + + + +class AskStreamCommand(sublime_plugin.WindowCommand): + + + def on_message_callback(self,message: QGPTQuestionOutput): + print(message) diff --git a/copilot/ask_websocket.py b/copilot/ask_websocket.py index 2a6b8c1..6a44eb1 100644 --- a/copilot/ask_websocket.py +++ b/copilot/ask_websocket.py @@ -1,5 +1,4 @@ from pieces_os_client import QGPTStreamOutput -import threading from ..settings import PiecesSettings from ..base_websocket import BaseWebsocket @@ -7,7 +6,7 @@ class AskStreamWS(BaseWebsocket): def __new__(cls,*args,**kwargs): if not hasattr(cls, 'instance'): - cls.instance = super(AssetsIdentifiersWS, cls).__new__(cls) + cls.instance = super(AskStreamWS, cls).__new__(cls) return cls.instance @property diff --git a/main.py b/main.py index 6956ea5..77235fb 100644 --- a/main.py +++ b/main.py @@ -11,6 +11,7 @@ from .auth import * from .search import * from .misc import * +from .copilot import * from .base_websocket import BaseWebsocket PiecesSettings.host_init() # Intilize the hosts url @@ -43,6 +44,9 @@ def startup(): PiecesSettings.create_auth_output_panel() AuthWebsocket(AuthUser.on_user_callback).start() # Load the stream user websocket + # Ask Stream Websocket + AskStreamWS(AskStreamCommand.on_message_callback).start() + def plugin_loaded(): sublime.set_timeout_async(startup,0) From 187606cb183d45e8906144b8a1a1e677b1b8edac Mon Sep 17 00:00:00 2001 From: Bishoy-at-pieces Date: Mon, 10 Jun 2024 21:52:45 +0300 Subject: [PATCH 03/30] add the gpt view --- Pieces.sublime-commands | 4 +++ copilot/__init__.py | 2 +- copilot/ask_command.py | 57 +++++++++++++++++++++++++++--- copilot/ask_websocket.py | 8 ++++- event_listener.py | 8 +++++ keybindings/Default.sublime-keymap | 8 +++++ main.py | 2 +- 7 files changed, 81 insertions(+), 8 deletions(-) create mode 100644 keybindings/Default.sublime-keymap diff --git a/Pieces.sublime-commands b/Pieces.sublime-commands index 52edbd2..2609926 100644 --- a/Pieces.sublime-commands +++ b/Pieces.sublime-commands @@ -31,5 +31,9 @@ { "caption": "Pieces: Reload Plugin", "command": "pieces_reload" + }, + { + "caption": "Pieces: Ask", + "command": "pieces_ask_stream" } ] \ No newline at end of file diff --git a/copilot/__init__.py b/copilot/__init__.py index a5620cc..8779774 100644 --- a/copilot/__init__.py +++ b/copilot/__init__.py @@ -1,2 +1,2 @@ from .ask_websocket import AskStreamWS -from .ask_command import AskStreamCommand \ No newline at end of file +from .ask_command import PiecesAskStreamCommand,PiecesEnterResponse \ No newline at end of file diff --git a/copilot/ask_command.py b/copilot/ask_command.py index eb07909..9588c16 100644 --- a/copilot/ask_command.py +++ b/copilot/ask_command.py @@ -1,10 +1,57 @@ -from pieces_os_client import QGPTQuestionOutput +from pieces_os_client import QGPTQuestionInput, QGPTStreamInput, QGPTStreamOutput, RelevantQGPTSeed, RelevantQGPTSeeds +from ..settings import PiecesSettings +from sublime import Region import sublime_plugin +from .ask_websocket import AskStreamWS +class PiecesAskStreamCommand(sublime_plugin.WindowCommand): + gpt_view = None -class AskStreamCommand(sublime_plugin.WindowCommand): - + def run(self): + if self.gpt_view: + try: + self.window.focus_view(self.gpt_view) + return + except Exception as e: + print(e) + self.gpt_view = None + PiecesAskStreamCommand.gpt_view = self.window.new_file(syntax="Packages/Markdown/Markdown.sublime-syntax") + + self.gpt_view.settings().set("PIECES_GPT_VIEW",True) # Label the view as gpt view + self.gpt_view.set_scratch(True) + self.show_cursor() - def on_message_callback(self,message: QGPTQuestionOutput): - print(message) + def show_cursor(self): + self.gpt_view.run_command("append",{"characters":">>> "}) + + def on_message_callback(self,message: QGPTStreamOutput): + if message.question: + answers = message.question.answers.iterable + + for answer in answers: + text = answer.text + self.gpt_view.run_command("append",{"characters":text}) + if message.status == "COMPLETED": + PiecesEnterResponse.end_response = self.gpt_view.size() # Update the size + self.gpt_view.set_read_only(False) + self.show_cursor() + +class PiecesEnterResponse(sublime_plugin.TextCommand): + conversation_id = None + end_response = 0 # Store the region of the response + + def run(self,edit): + PiecesAskStreamCommand.gpt_view.set_read_only(True) + AskStreamWS().send_message( + QGPTStreamInput( + question=QGPTQuestionInput( + query = self.view.substr(Region(self.end_response,self.view.size())), + relevant = RelevantQGPTSeeds(iterable=[]), + application=PiecesSettings.get_application().id, + model = PiecesSettings.model_id + ), + conversation = self.conversation_id, + + ) + ) \ No newline at end of file diff --git a/copilot/ask_websocket.py b/copilot/ask_websocket.py index 6a44eb1..35ca832 100644 --- a/copilot/ask_websocket.py +++ b/copilot/ask_websocket.py @@ -1,4 +1,4 @@ -from pieces_os_client import QGPTStreamOutput +from pieces_os_client import QGPTStreamOutput,QGPTStreamInput from ..settings import PiecesSettings from ..base_websocket import BaseWebsocket @@ -16,3 +16,9 @@ def url(self): def on_message(self,ws, message): self.on_message_callback(QGPTStreamOutput.from_json(message)) + + def send_message(self,message:QGPTStreamInput): + if not self.running: + self.start() + + self.ws.send(message.to_json()) diff --git a/event_listener.py b/event_listener.py index 2a2fae2..c797814 100644 --- a/event_listener.py +++ b/event_listener.py @@ -36,3 +36,11 @@ def on_pre_close(self,view): view.window().run_command("pieces_handle_markdown",{"mode": "save"}) return view.window().run_command("pieces_list_assets",{"pieces_asset_id":asset_id}) + + + def on_query_context(self,view,key, operator, operand, match_all): + print(key) + if key != "PIECES_GPT_VIEW": + return None + return view.settings().get("PIECES_GPT_VIEW") + \ No newline at end of file diff --git a/keybindings/Default.sublime-keymap b/keybindings/Default.sublime-keymap new file mode 100644 index 0000000..a865461 --- /dev/null +++ b/keybindings/Default.sublime-keymap @@ -0,0 +1,8 @@ +[ + { + "keys": ["enter"], + "command": "pieces_enter_response", + "context": [ + {"key": "PIECES_GPT_VIEW"}] + } +] \ No newline at end of file diff --git a/main.py b/main.py index 77235fb..be1c39c 100644 --- a/main.py +++ b/main.py @@ -45,7 +45,7 @@ def startup(): AuthWebsocket(AuthUser.on_user_callback).start() # Load the stream user websocket # Ask Stream Websocket - AskStreamWS(AskStreamCommand.on_message_callback).start() + AskStreamWS(PiecesAskStreamCommand.on_message_callback).start() def plugin_loaded(): From 41e74b5e3e21b5a7024a241c65797e31ebad9bb4 Mon Sep 17 00:00:00 2001 From: Bishoy-at-pieces Date: Mon, 10 Jun 2024 22:24:14 +0300 Subject: [PATCH 04/30] remove print statement --- copilot/ask_command.py | 2 +- copilot/ask_websocket.py | 9 ++++++--- event_listener.py | 1 - 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/copilot/ask_command.py b/copilot/ask_command.py index 9588c16..ad8a8ae 100644 --- a/copilot/ask_command.py +++ b/copilot/ask_command.py @@ -43,7 +43,7 @@ class PiecesEnterResponse(sublime_plugin.TextCommand): def run(self,edit): PiecesAskStreamCommand.gpt_view.set_read_only(True) - AskStreamWS().send_message( + AskStreamWS(PiecesAskStreamCommand.on_message_callback).send_message( QGPTStreamInput( question=QGPTQuestionInput( query = self.view.substr(Region(self.end_response,self.view.size())), diff --git a/copilot/ask_websocket.py b/copilot/ask_websocket.py index 35ca832..ea0d354 100644 --- a/copilot/ask_websocket.py +++ b/copilot/ask_websocket.py @@ -1,4 +1,5 @@ from pieces_os_client import QGPTStreamOutput,QGPTStreamInput +from websocket import WebSocketConnectionClosedException from ..settings import PiecesSettings from ..base_websocket import BaseWebsocket @@ -18,7 +19,9 @@ def on_message(self,ws, message): def send_message(self,message:QGPTStreamInput): - if not self.running: + try: + if not self.ws: + raise WebSocketConnectionClosedException() + self.ws.send(message.to_json()) + except WebSocketConnectionClosedException: self.start() - - self.ws.send(message.to_json()) diff --git a/event_listener.py b/event_listener.py index c797814..11322e3 100644 --- a/event_listener.py +++ b/event_listener.py @@ -39,7 +39,6 @@ def on_pre_close(self,view): def on_query_context(self,view,key, operator, operand, match_all): - print(key) if key != "PIECES_GPT_VIEW": return None return view.settings().get("PIECES_GPT_VIEW") From c2599ca4d1245e4e3f89e19df38540ab97620f92 Mon Sep 17 00:00:00 2001 From: Bishoy-at-pieces Date: Tue, 11 Jun 2024 02:05:52 +0300 Subject: [PATCH 05/30] fix storage views and add keybindings --- copilot/__init__.py | 3 +- copilot/ask_command.py | 57 +++------------ copilot/ask_view.py | 86 ++++++++++++++++++++++ copilot/ask_websocket.py | 3 +- event_listener.py | 20 ++++-- keybindings/Default.sublime-keymap | 111 ++++++++++++++++++++++++++++- main.py | 2 - 7 files changed, 224 insertions(+), 58 deletions(-) create mode 100644 copilot/ask_view.py diff --git a/copilot/__init__.py b/copilot/__init__.py index 8779774..6857ad2 100644 --- a/copilot/__init__.py +++ b/copilot/__init__.py @@ -1,2 +1 @@ -from .ask_websocket import AskStreamWS -from .ask_command import PiecesAskStreamCommand,PiecesEnterResponse \ No newline at end of file +from .ask_command import PiecesAskStreamCommand,PiecesEnterResponseCommand \ No newline at end of file diff --git a/copilot/ask_command.py b/copilot/ask_command.py index ad8a8ae..0827700 100644 --- a/copilot/ask_command.py +++ b/copilot/ask_command.py @@ -1,57 +1,18 @@ -from pieces_os_client import QGPTQuestionInput, QGPTStreamInput, QGPTStreamOutput, RelevantQGPTSeed, RelevantQGPTSeeds -from ..settings import PiecesSettings -from sublime import Region import sublime_plugin -from .ask_websocket import AskStreamWS +from .ask_view import CopilotViewManager +copilot = CopilotViewManager() -class PiecesAskStreamCommand(sublime_plugin.WindowCommand): - gpt_view = None +class PiecesAskStreamCommand(sublime_plugin.WindowCommand): def run(self): - if self.gpt_view: - try: - self.window.focus_view(self.gpt_view) - return - except Exception as e: - print(e) - self.gpt_view = None - PiecesAskStreamCommand.gpt_view = self.window.new_file(syntax="Packages/Markdown/Markdown.sublime-syntax") - - self.gpt_view.settings().set("PIECES_GPT_VIEW",True) # Label the view as gpt view - self.gpt_view.set_scratch(True) - self.show_cursor() + copilot.ask_websocket.start() + self.window.focus_view(copilot.gpt_view) + return - def show_cursor(self): - self.gpt_view.run_command("append",{"characters":">>> "}) - def on_message_callback(self,message: QGPTStreamOutput): - if message.question: - answers = message.question.answers.iterable - for answer in answers: - text = answer.text - self.gpt_view.run_command("append",{"characters":text}) - if message.status == "COMPLETED": - PiecesEnterResponse.end_response = self.gpt_view.size() # Update the size - self.gpt_view.set_read_only(False) - self.show_cursor() - -class PiecesEnterResponse(sublime_plugin.TextCommand): - conversation_id = None - end_response = 0 # Store the region of the response +class PiecesEnterResponseCommand(sublime_plugin.TextCommand): + def run(self,_): + copilot.ask() - def run(self,edit): - PiecesAskStreamCommand.gpt_view.set_read_only(True) - AskStreamWS(PiecesAskStreamCommand.on_message_callback).send_message( - QGPTStreamInput( - question=QGPTQuestionInput( - query = self.view.substr(Region(self.end_response,self.view.size())), - relevant = RelevantQGPTSeeds(iterable=[]), - application=PiecesSettings.get_application().id, - model = PiecesSettings.model_id - ), - conversation = self.conversation_id, - - ) - ) \ No newline at end of file diff --git a/copilot/ask_view.py b/copilot/ask_view.py new file mode 100644 index 0000000..c2da818 --- /dev/null +++ b/copilot/ask_view.py @@ -0,0 +1,86 @@ +import sublime +from .ask_websocket import AskStreamWS +from pieces_os_client import QGPTStreamOutput +from pieces_os_client import QGPTQuestionInput, QGPTStreamInput, RelevantQGPTSeeds +from ..settings import PiecesSettings +from sublime import Region + +class CopilotViewManager: + _gpt_view = None # current view for the ask stream + _conversation_id = None + can_type = True + + @property + def gpt_view(self) -> sublime.View: + views = [view for window in sublime.windows() for view in window.views()] + if not self._gpt_view in views: + CopilotViewManager._gpt_view = sublime.active_window().new_file(syntax="Packages/Markdown/Markdown.sublime-syntax") + CopilotViewManager.can_type = True + self._gpt_view.settings().set("PIECES_GPT_VIEW",True) # Label the view as gpt view + self._gpt_view.settings().set("end_response",0) # End reponse charater + self._gpt_view.set_scratch(True) + self._gpt_view.set_name("Pieces Copilot") + self.show_cursor + return self._gpt_view + + @gpt_view.setter + def gpt_view(self,view): + self._gpt_view = view + + @property + def show_cursor(self): + self.gpt_view.run_command("append",{"characters":">>> "}) + self.gpt_view.settings().set("end_response",self.end_response+4) # ">>> " 4 characters + self.select_end + + @property + def end_response(self) -> int: + return self.gpt_view.settings().get("end_response") + + + def on_message_callback(self,message: QGPTStreamOutput): + if message.question: + answers = message.question.answers.iterable + + for answer in answers: + print(answer.text) + self.gpt_view.run_command("append",{"characters":answer.text}) + + if message.status == "COMPLETED": + self.gpt_view.settings().set("end_response",self.gpt_view.size()) # Update the size + self.new_line() + self.show_cursor + CopilotViewManager.can_type = True + self._conversation_id = message.conversation + + @property + def select_end(self) -> None: + self.gpt_view.run_command('move_to', {"to": "eof"}) + + + def new_line(self,lines = 2) -> None: + for _ in range(lines): + self.gpt_view.run_command("append",{"characters":"\n"}) + + def ask(self,relevant=RelevantQGPTSeeds(iterable=[])): + CopilotViewManager.can_type = False + self.new_line() + self.ask_websocket.send_message( + QGPTStreamInput( + question=QGPTQuestionInput( + query = self.gpt_view.substr(Region(self.end_response,self.gpt_view.size())), + relevant = relevant, + application=PiecesSettings.get_application().id, + model = PiecesSettings.model_id + ), + conversation = self._conversation_id, + + )) + + @property + def ask_websocket(self): + if not hasattr(self,"_ask_websocket"): + CopilotViewManager._ask_websocket = AskStreamWS(self.on_message_callback) + return self._ask_websocket + + \ No newline at end of file diff --git a/copilot/ask_websocket.py b/copilot/ask_websocket.py index ea0d354..669da48 100644 --- a/copilot/ask_websocket.py +++ b/copilot/ask_websocket.py @@ -24,4 +24,5 @@ def send_message(self,message:QGPTStreamInput): raise WebSocketConnectionClosedException() self.ws.send(message.to_json()) except WebSocketConnectionClosedException: - self.start() + self.start() # Start a new websocket since we are not connected to any + self.on_open = lambda ws: ws.send(message.to_json()) # Send the message on opening \ No newline at end of file diff --git a/event_listener.py b/event_listener.py index 11322e3..cef494f 100644 --- a/event_listener.py +++ b/event_listener.py @@ -5,6 +5,7 @@ from .assets.list_assets import PiecesListAssetsCommand from .settings import PiecesSettings from .base_websocket import BaseWebsocket +from .copilot.ask_command import copilot class PiecesEventListener(sublime_plugin.EventListener): @@ -39,7 +40,18 @@ def on_pre_close(self,view): def on_query_context(self,view,key, operator, operand, match_all): - if key != "PIECES_GPT_VIEW": - return None - return view.settings().get("PIECES_GPT_VIEW") - \ No newline at end of file + if key == "PIECES_GPT_VIEW": + return view.settings().get("PIECES_GPT_VIEW") + + elif key == "pieces_copilot": + if view.settings().get("PIECES_GPT_VIEW"): + copilot.select_end + return not copilot.can_type + else: + return False + + def on_init(self,views): + for view in views: + if view.settings().get("PIECES_GPT_VIEW"): + copilot.gpt_view = view # Set the old views + diff --git a/keybindings/Default.sublime-keymap b/keybindings/Default.sublime-keymap index a865461..8730704 100644 --- a/keybindings/Default.sublime-keymap +++ b/keybindings/Default.sublime-keymap @@ -1,8 +1,117 @@ [ + // Enter for new response { "keys": ["enter"], "command": "pieces_enter_response", "context": [ {"key": "PIECES_GPT_VIEW"}] - } + }, + + + // KEYS + { "keys": ["tab"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["space"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["backspace"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["enter"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["shift+insert"], "command": "noop","context": [{"key": "pieces_copilot"}]}, + { "keys": ["shift+tab"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["shift+backspace"], "command": "noop","context": [{"key": "pieces_copilot"}]}, + + + // Basic keys + { "keys": ["A"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["B"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["C"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["D"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["E"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["F"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["G"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["H"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["I"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["J"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["K"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["L"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["M"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["N"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["O"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["P"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["Q"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["R"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["S"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["T"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["U"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["V"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["W"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["X"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["Y"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["Z"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["a"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["b"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["c"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["d"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["e"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["f"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["g"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["h"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["i"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["j"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["k"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["l"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["m"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["n"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["o"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["p"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["q"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["r"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["s"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["t"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["u"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["v"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["w"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["x"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["y"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["z"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["["], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": [" "], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["!"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["\""],"command": "noop","context": [{"key": "pieces_copilot"}]}, + { "keys": ["#"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["$"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["%"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["&"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["'"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["("], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": [")"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["*"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["+"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": [","], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["-"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["."], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["/"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["0"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["1"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["2"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["3"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["4"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["5"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["6"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["7"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["8"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["9"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": [":"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": [";"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["<"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["="], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": [">"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["?"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["@"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["]"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["^"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["_"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["`"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["{"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["|"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["}"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["~"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["\\"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, ] \ No newline at end of file diff --git a/main.py b/main.py index be1c39c..f7da6b0 100644 --- a/main.py +++ b/main.py @@ -44,8 +44,6 @@ def startup(): PiecesSettings.create_auth_output_panel() AuthWebsocket(AuthUser.on_user_callback).start() # Load the stream user websocket - # Ask Stream Websocket - AskStreamWS(PiecesAskStreamCommand.on_message_callback).start() def plugin_loaded(): From 5eb9fec578d6f474d1de21fcb688c5518e2d37fa Mon Sep 17 00:00:00 2001 From: Bishoy-at-pieces Date: Tue, 11 Jun 2024 02:29:45 +0300 Subject: [PATCH 06/30] add model status --- copilot/ask_view.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/copilot/ask_view.py b/copilot/ask_view.py index c2da818..ef62e9a 100644 --- a/copilot/ask_view.py +++ b/copilot/ask_view.py @@ -12,16 +12,18 @@ class CopilotViewManager: @property def gpt_view(self) -> sublime.View: - views = [view for window in sublime.windows() for view in window.views()] - if not self._gpt_view in views: + + if not self._gpt_view: CopilotViewManager._gpt_view = sublime.active_window().new_file(syntax="Packages/Markdown/Markdown.sublime-syntax") CopilotViewManager.can_type = True self._gpt_view.settings().set("PIECES_GPT_VIEW",True) # Label the view as gpt view self._gpt_view.settings().set("end_response",0) # End reponse charater self._gpt_view.set_scratch(True) self._gpt_view.set_name("Pieces Copilot") + self._gpt_view.set_status("MODEL",PiecesSettings.model_name) self.show_cursor return self._gpt_view + @gpt_view.setter def gpt_view(self,view): @@ -29,6 +31,7 @@ def gpt_view(self,view): @property def show_cursor(self): + self.gpt_view.set_status("MODEL",PiecesSettings.model_name) self.gpt_view.run_command("append",{"characters":">>> "}) self.gpt_view.settings().set("end_response",self.end_response+4) # ">>> " 4 characters self.select_end @@ -43,12 +46,11 @@ def on_message_callback(self,message: QGPTStreamOutput): answers = message.question.answers.iterable for answer in answers: - print(answer.text) self.gpt_view.run_command("append",{"characters":answer.text}) if message.status == "COMPLETED": - self.gpt_view.settings().set("end_response",self.gpt_view.size()) # Update the size self.new_line() + self.gpt_view.settings().set("end_response",self.gpt_view.size()) # Update the size self.show_cursor CopilotViewManager.can_type = True self._conversation_id = message.conversation From 0213171a55b802e91ecd0837fedeeec75481be86 Mon Sep 17 00:00:00 2001 From: Bishoy-at-pieces Date: Tue, 11 Jun 2024 02:29:56 +0300 Subject: [PATCH 07/30] fix some issue and add on load --- copilot/ask_command.py | 7 ++++++- event_listener.py | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/copilot/ask_command.py b/copilot/ask_command.py index 0827700..726d64a 100644 --- a/copilot/ask_command.py +++ b/copilot/ask_command.py @@ -1,5 +1,6 @@ import sublime_plugin from .ask_view import CopilotViewManager +from ..settings import PiecesSettings copilot = CopilotViewManager() @@ -9,10 +10,14 @@ def run(self): copilot.ask_websocket.start() self.window.focus_view(copilot.gpt_view) return - + + def is_enabled(self): + return PiecesSettings().is_loaded class PiecesEnterResponseCommand(sublime_plugin.TextCommand): def run(self,_): copilot.ask() + def is_enabled(self): + return PiecesSettings().is_loaded \ No newline at end of file diff --git a/event_listener.py b/event_listener.py index cef494f..6cdc623 100644 --- a/event_listener.py +++ b/event_listener.py @@ -55,3 +55,7 @@ def on_init(self,views): if view.settings().get("PIECES_GPT_VIEW"): copilot.gpt_view = view # Set the old views +class PiecesViewEventListener(sublime_plugin.ViewEventListener): + def on_close(self): + if self.view.settings().get("PIECES_GPT_VIEW"): + copilot.gpt_view = None \ No newline at end of file From f238be2e550a3608f05ed0e90a52c14679d1ba8c Mon Sep 17 00:00:00 2001 From: Bishoy-at-pieces Date: Tue, 11 Jun 2024 19:56:29 +0300 Subject: [PATCH 08/30] fix some issues --- copilot/ask_view.py | 37 +++++++++++++++++++----------- keybindings/Default.sublime-keymap | 2 +- main.py | 5 +++- settings.py | 5 +++- 4 files changed, 32 insertions(+), 17 deletions(-) diff --git a/copilot/ask_view.py b/copilot/ask_view.py index ef62e9a..366e1a9 100644 --- a/copilot/ask_view.py +++ b/copilot/ask_view.py @@ -6,28 +6,30 @@ from sublime import Region class CopilotViewManager: - _gpt_view = None # current view for the ask stream - _conversation_id = None can_type = True @property def gpt_view(self) -> sublime.View: - - if not self._gpt_view: + if not getattr(CopilotViewManager, "_gpt_view",None): CopilotViewManager._gpt_view = sublime.active_window().new_file(syntax="Packages/Markdown/Markdown.sublime-syntax") CopilotViewManager.can_type = True - self._gpt_view.settings().set("PIECES_GPT_VIEW",True) # Label the view as gpt view - self._gpt_view.settings().set("end_response",0) # End reponse charater - self._gpt_view.set_scratch(True) - self._gpt_view.set_name("Pieces Copilot") - self._gpt_view.set_status("MODEL",PiecesSettings.model_name) + CopilotViewManager._gpt_view.settings().set("PIECES_GPT_VIEW",True) # Label the view as gpt view + CopilotViewManager._gpt_view.settings().set("end_response",0) # End reponse charater + CopilotViewManager._gpt_view.set_scratch(True) + CopilotViewManager._gpt_view.set_name("Pieces Copilot") self.show_cursor - return self._gpt_view + self.update_status_bar() + return CopilotViewManager._gpt_view @gpt_view.setter def gpt_view(self,view): - self._gpt_view = view + CopilotViewManager._gpt_view = view + + + def update_status_bar(self): + if getattr(self,"_gpt_view",None): + self._gpt_view.set_status("MODEL",f"LLM Model: {PiecesSettings.model_name.replace('Chat Model','')}") @property def show_cursor(self): @@ -53,7 +55,15 @@ def on_message_callback(self,message: QGPTStreamOutput): self.gpt_view.settings().set("end_response",self.gpt_view.size()) # Update the size self.show_cursor CopilotViewManager.can_type = True - self._conversation_id = message.conversation + self.conversation_id = message.conversation + + @property + def conversation_id(self): + return self.gpt_view.settings().get("conversation_id") + + @conversation_id.setter + def conversation_id(self,id): + self.gpt_view.settings().set("conversation_id",id) @property def select_end(self) -> None: @@ -75,7 +85,7 @@ def ask(self,relevant=RelevantQGPTSeeds(iterable=[])): application=PiecesSettings.get_application().id, model = PiecesSettings.model_id ), - conversation = self._conversation_id, + conversation = self.conversation_id, )) @@ -85,4 +95,3 @@ def ask_websocket(self): CopilotViewManager._ask_websocket = AskStreamWS(self.on_message_callback) return self._ask_websocket - \ No newline at end of file diff --git a/keybindings/Default.sublime-keymap b/keybindings/Default.sublime-keymap index 8730704..3e2cec9 100644 --- a/keybindings/Default.sublime-keymap +++ b/keybindings/Default.sublime-keymap @@ -16,7 +16,7 @@ { "keys": ["shift+insert"], "command": "noop","context": [{"key": "pieces_copilot"}]}, { "keys": ["shift+tab"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, { "keys": ["shift+backspace"], "command": "noop","context": [{"key": "pieces_copilot"}]}, - + { "keys": ["ctrl+backspace"], "command": "noop","context": [{"key": "pieces_copilot"}]}, // Basic keys { "keys": ["A"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, diff --git a/main.py b/main.py index 97f5e9e..38074b0 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,7 @@ from . import __version__ from .api import open_pieces_os,print_version_details,version_check from .settings import PiecesSettings - +from .copilot.ask_command import copilot import sublime # load the commands @@ -50,6 +50,9 @@ def plugin_loaded(): settings.add_on_change("PIECES_SETTINGS",PiecesSettings.on_settings_change) PiecesSettings.host_init(host) # Intilize the hosts url + # callbacks needed onchange settings + PiecesSettings.on_model_change_callbacks.append(copilot.update_status_bar) + sublime.set_timeout_async(lambda : startup(model) ,0) diff --git a/settings.py b/settings.py index b8bb5e8..5c89a3a 100644 --- a/settings.py +++ b/settings.py @@ -15,6 +15,7 @@ class PiecesSettings: api_client = None _is_loaded = False # is the plugin loaded + on_model_change_callbacks = [] # If the model change a function should be runned @@ -92,7 +93,9 @@ def models_init(cls,model): if not cls.model_id: cls.model_id = models["GPT-3.5-turbo Chat Model"] - + print(cls.on_model_change_callbacks) + for func in cls.on_model_change_callbacks: + func() @classmethod def on_settings_change(cls,all = False): From 7f2489317082592434d9261f5d7547674c4ae29b Mon Sep 17 00:00:00 2001 From: Bishoy-at-pieces Date: Tue, 11 Jun 2024 21:21:41 +0300 Subject: [PATCH 09/30] Fix some issues with the selection --- event_listener.py | 24 +++- keybindings/Default.sublime-keymap | 209 +++++++++++++++-------------- 2 files changed, 127 insertions(+), 106 deletions(-) diff --git a/event_listener.py b/event_listener.py index 6cdc623..23711f4 100644 --- a/event_listener.py +++ b/event_listener.py @@ -43,10 +43,28 @@ def on_query_context(self,view,key, operator, operand, match_all): if key == "PIECES_GPT_VIEW": return view.settings().get("PIECES_GPT_VIEW") - elif key == "pieces_copilot": + elif key == "pieces_copilot_add" or key == "pieces_copilot_remove": + ## TRUE -> Means no operation will be done + ## False -> Means operation can be done + if view.settings().get("PIECES_GPT_VIEW"): - copilot.select_end - return not copilot.can_type + print(copilot.end_response, view.sel()[0].begin()) + if not copilot.can_type: + return True # If we can't type then don't accpet operations + + # Set the selection at the begining if we are in the middle of a view + if copilot.end_response > view.sel()[0].begin(): + copilot.select_end + + + # Mange the remove to avoid removing the main reponse + if key == "pieces_copilot_remove" and copilot.end_response >= view.sel()[0].begin(): + return True + + elif key == "pieces_copilot_add" and copilot.end_response > view.sel()[0].begin(): + return True + + return False # All cheks is done you can enter what you want! else: return False diff --git a/keybindings/Default.sublime-keymap b/keybindings/Default.sublime-keymap index 3e2cec9..fc9c6df 100644 --- a/keybindings/Default.sublime-keymap +++ b/keybindings/Default.sublime-keymap @@ -1,4 +1,7 @@ [ + // Not ment to be edited by any user! + // All of these keybindings works on the copilot view + // Enter for new response { "keys": ["enter"], @@ -9,109 +12,109 @@ // KEYS - { "keys": ["tab"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["space"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["backspace"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["enter"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["shift+insert"], "command": "noop","context": [{"key": "pieces_copilot"}]}, - { "keys": ["shift+tab"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["shift+backspace"], "command": "noop","context": [{"key": "pieces_copilot"}]}, - { "keys": ["ctrl+backspace"], "command": "noop","context": [{"key": "pieces_copilot"}]}, + { "keys": ["tab"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["space"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["backspace"], "command": "noop", "context": [{"key": "pieces_copilot_remove"}]}, + { "keys": ["enter"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["shift+insert"], "command": "noop","context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["shift+tab"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["shift+backspace"], "command": "noop","context": [{"key": "pieces_copilot_remove"}]}, + { "keys": ["ctrl+backspace"], "command": "noop","context": [{"key": "pieces_copilot_remove"}]}, // Basic keys - { "keys": ["A"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["B"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["C"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["D"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["E"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["F"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["G"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["H"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["I"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["J"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["K"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["L"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["M"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["N"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["O"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["P"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["Q"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["R"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["S"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["T"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["U"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["V"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["W"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["X"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["Y"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["Z"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["a"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["b"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["c"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["d"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["e"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["f"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["g"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["h"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["i"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["j"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["k"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["l"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["m"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["n"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["o"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["p"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["q"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["r"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["s"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["t"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["u"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["v"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["w"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["x"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["y"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["z"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["["], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": [" "], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["!"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["\""],"command": "noop","context": [{"key": "pieces_copilot"}]}, - { "keys": ["#"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["$"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["%"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["&"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["'"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["("], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": [")"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["*"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["+"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": [","], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["-"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["."], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["/"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["0"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["1"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["2"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["3"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["4"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["5"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["6"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["7"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["8"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["9"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": [":"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": [";"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["<"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["="], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": [">"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["?"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["@"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["]"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["^"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["_"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["`"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["{"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["|"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["}"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["~"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, - { "keys": ["\\"], "command": "noop", "context": [{"key": "pieces_copilot"}]}, + { "keys": ["A"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["B"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["C"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["D"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["E"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["F"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["G"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["H"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["I"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["J"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["K"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["L"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["M"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["N"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["O"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["P"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["Q"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["R"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["S"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["T"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["U"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["V"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["W"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["X"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["Y"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["Z"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["a"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["b"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["c"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["d"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["e"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["f"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["g"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["h"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["i"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["j"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["k"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["l"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["m"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["n"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["o"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["p"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["q"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["r"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["s"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["t"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["u"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["v"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["w"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["x"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["y"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["z"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["["], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": [" "], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["!"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["\""],"command": "noop","context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["#"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["$"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["%"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["&"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["'"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["("], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": [")"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["*"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["+"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": [","], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["-"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["."], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["/"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["0"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["1"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["2"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["3"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["4"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["5"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["6"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["7"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["8"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["9"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": [":"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": [";"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["<"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["="], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": [">"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["?"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["@"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["]"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["^"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["_"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["`"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["{"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["|"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["}"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["~"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, + { "keys": ["\\"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, ] \ No newline at end of file From ef0aad4ef7d9050dbf9e875b9d552c8b4d9daab4 Mon Sep 17 00:00:00 2001 From: Bishoy-at-pieces Date: Tue, 11 Jun 2024 21:22:53 +0300 Subject: [PATCH 10/30] remove print statement --- event_listener.py | 1 - 1 file changed, 1 deletion(-) diff --git a/event_listener.py b/event_listener.py index 23711f4..147aa75 100644 --- a/event_listener.py +++ b/event_listener.py @@ -48,7 +48,6 @@ def on_query_context(self,view,key, operator, operand, match_all): ## False -> Means operation can be done if view.settings().get("PIECES_GPT_VIEW"): - print(copilot.end_response, view.sel()[0].begin()) if not copilot.can_type: return True # If we can't type then don't accpet operations From 1de5c48d69a03c0a2f8d954e1b5433dfb2dd0e04 Mon Sep 17 00:00:00 2001 From: Bishoy-at-pieces Date: Wed, 12 Jun 2024 15:00:07 +0300 Subject: [PATCH 11/30] fix copilot buttons styles --- copilot/ask_view.py | 67 +++++++++++++++++++++++++++++++++++++++++++-- settings.py | 9 ++++-- 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/copilot/ask_view.py b/copilot/ask_view.py index 366e1a9..8c5bfe6 100644 --- a/copilot/ask_view.py +++ b/copilot/ask_view.py @@ -1,9 +1,19 @@ import sublime from .ask_websocket import AskStreamWS -from pieces_os_client import QGPTStreamOutput -from pieces_os_client import QGPTQuestionInput, QGPTStreamInput, RelevantQGPTSeeds +from pieces_os_client import QGPTQuestionInput, QGPTStreamInput, RelevantQGPTSeeds,QGPTStreamOutput from ..settings import PiecesSettings from sublime import Region +import re + + +PHANTOM_A_TAG_STYLE = "padding: 4px;background-color: var(--accent); border-radius: 6px;color: var(--foreground);text-decoration: None;text-align: center" + +PHANTOM_CONTENT = f""" +
+ Save + Copy +
+""" class CopilotViewManager: can_type = True @@ -11,12 +21,22 @@ class CopilotViewManager: @property def gpt_view(self) -> sublime.View: if not getattr(CopilotViewManager, "_gpt_view",None): + + # File config and creation CopilotViewManager._gpt_view = sublime.active_window().new_file(syntax="Packages/Markdown/Markdown.sublime-syntax") CopilotViewManager.can_type = True CopilotViewManager._gpt_view.settings().set("PIECES_GPT_VIEW",True) # Label the view as gpt view CopilotViewManager._gpt_view.settings().set("end_response",0) # End reponse charater CopilotViewManager._gpt_view.set_scratch(True) CopilotViewManager._gpt_view.set_name("Pieces Copilot") + + # Phantom intilization + self.phantom_set = sublime.PhantomSet(CopilotViewManager._gpt_view, "Pieces_Phantoms") + self.last_edit_phantom = 0 + self.code_blocks_dict = {} # id: code + + + # Others self.show_cursor self.update_status_bar() return CopilotViewManager._gpt_view @@ -56,7 +76,7 @@ def on_message_callback(self,message: QGPTStreamOutput): self.show_cursor CopilotViewManager.can_type = True self.conversation_id = message.conversation - + self.add_code_phantoms() # Generate the code phantoms @property def conversation_id(self): return self.gpt_view.settings().get("conversation_id") @@ -76,6 +96,7 @@ def new_line(self,lines = 2) -> None: def ask(self,relevant=RelevantQGPTSeeds(iterable=[])): CopilotViewManager.can_type = False + self.select_end # got to the end of the text to enter the new lines self.new_line() self.ask_websocket.send_message( QGPTStreamInput( @@ -95,3 +116,43 @@ def ask_websocket(self): CopilotViewManager._ask_websocket = AskStreamWS(self.on_message_callback) return self._ask_websocket + + + def add_code_phantoms(self): + view = self.gpt_view + + content = view.substr(sublime.Region(self.last_edit_phantom, view.size())) + + # Regular expression to find code blocks in Markdown + code_block_pattern = re.compile(r'```.*?\n(.*?)```', re.DOTALL) + matches = code_block_pattern.finditer(content) + + + phantoms = [] + + for match in matches: + id = str(len(self.code_blocks_dict)) + # Create a phantom at the end of each code block + self.code_blocks_dict[id] = match.group(1) + end_point = match.end() + phantom = sublime.Phantom( + sublime.Region(end_point, end_point), + PHANTOM_CONTENT.format(id), + sublime.LAYOUT_BELOW, + on_navigate=self.on_nav + ) + phantoms.append(phantom) + + self.phantom_set.update(phantoms) + + self.last_edit_phantom = view.size() + + def on_nav(self,href:str): + command,id = href.split("_") + code = self.code_blocks_dict[id] + + if command == "save": + self.gpt_view.run_command("pieces_create_asset",{"data":code}) + elif command == "copy": + sublime.set_clipboard(code) + diff --git a/settings.py b/settings.py index 5c89a3a..92b548b 100644 --- a/settings.py +++ b/settings.py @@ -1,6 +1,7 @@ import pieces_os_client as pos_client import sublime from typing import Dict +import os from . import __version__ @@ -14,9 +15,14 @@ class PiecesSettings: model_name = "" api_client = None _is_loaded = False # is the plugin loaded - + on_model_change_callbacks = [] # If the model change a function should be runned + PIECES_USER_DIRECTORY = os.path.join(sublime.packages_path(),"User","Pieces") + + # Create the pieces directory to store the data if it does not exists + if not os.path.exists(PIECES_USER_DIRECTORY): + os.makedirs(PIECES_USER_DIRECTORY) @property @@ -93,7 +99,6 @@ def models_init(cls,model): if not cls.model_id: cls.model_id = models["GPT-3.5-turbo Chat Model"] - print(cls.on_model_change_callbacks) for func in cls.on_model_change_callbacks: func() From d184fd578f6cf5448add90cbe1bf27d81b2d342c Mon Sep 17 00:00:00 2001 From: Bishoy-at-pieces Date: Wed, 12 Jun 2024 15:01:30 +0300 Subject: [PATCH 12/30] change to the streamed identifiers cache --- assets/list_assets.py | 4 +-- assets/markdown_handler.py | 2 +- assets/utils.py | 68 +++----------------------------------- main.py | 2 +- search/search_command.py | 2 +- streamed_identifiers.py | 66 ++++++++++++++++++++++++++++++++++++ 6 files changed, 76 insertions(+), 68 deletions(-) create mode 100644 streamed_identifiers.py diff --git a/assets/list_assets.py b/assets/list_assets.py index c823efa..2d38dfa 100644 --- a/assets/list_assets.py +++ b/assets/list_assets.py @@ -33,7 +33,7 @@ def run_async(self,pieces_asset_id): # Find all code blocks code_block = re.findall(code_block_pattern, markdown_text) try: - language = AssetSnapshot.assets_snapshot[pieces_asset_id].original.reference.classification.specific + language = identifiers_snapshot[pieces_asset_id].original.reference.classification.specific except: language = None PiecesListAssetsCommand.sheets_md[sheet_id] = {"code":"\n".join(code_block[0].split("\n")[1:-1]),"name":api_response.name,"language":language,"id":pieces_asset_id} @@ -51,7 +51,7 @@ def is_enabled(self): class PiecesAssetIdInputHandler(sublime_plugin.ListInputHandler): def list_items(self): - return self.get_assets_list(AssetSnapshot.assets_snapshot) + return self.get_assets_list(identifiers_snapshot) def get_assets_list(self,assets_snapshot): assets_list = [] diff --git a/assets/markdown_handler.py b/assets/markdown_handler.py index e9378e4..db37c64 100644 --- a/assets/markdown_handler.py +++ b/assets/markdown_handler.py @@ -47,7 +47,7 @@ def handle_save(self): if view: asset_id = PiecesHandleMarkdownCommand.views_to_handle.get(view.id()) if asset_id: - asset = AssetSnapshot.assets_snapshot[asset_id] + asset = identifiers_snapshot[asset_id] format_api = FormatApi(PiecesSettings.api_client) original = format_api.format_snapshot(asset.original.id, transferable=True) if original.classification.generic == ClassificationGenericEnum.IMAGE: diff --git a/assets/utils.py b/assets/utils.py index f725994..8a1dbcf 100644 --- a/assets/utils.py +++ b/assets/utils.py @@ -1,68 +1,10 @@ -import queue -from typing import Dict -import threading -from pieces_os_client import Asset, AssetApi,StreamedIdentifiers -import sublime - +from ..streamed_identifiers import StreamedIdentifiersCache from ..settings import PiecesSettings +from pieces_os_client import AssetApi - -class AssetSnapshot: - assets_snapshot:Dict[str,Asset] = {} # List of the asset object that is already loaded - asset_queue = queue.Queue() # Queue for asset_ids to be processed - block = True # to wait for the queue to recevive the first asset id - asset_set = set() # Set for asset_ids in the queue - first_shot = True # First time to open the websocket or not - - @classmethod - def worker(cls): - try: - while True: - asset_id = cls.asset_queue.get(block=cls.block,timeout=5) - cls.asset_set.remove(asset_id) # Remove asset_id from the set - cls.update_asset_id(asset_id) - cls.asset_queue.task_done() - except queue.Empty: # queue is empty and the block is false - if cls.block: - cls.worker() # if there is more assets to load - return # End the worker - - - - @classmethod - def update_asset_id(cls,asset_id): - api_instance = AssetApi(PiecesSettings.api_client) - asset = api_instance.asset_snapshot(asset_id) - cls.assets_snapshot[asset_id] = asset - - - @classmethod - def assets_snapshot_callback(cls,ids:StreamedIdentifiers): - # Start the worker thread if it's not running - cls.block = True - sublime.set_timeout_async(cls.worker) - for item in ids.iterable: - asset_id = item.asset.id - if asset_id not in cls.asset_set: - if item.deleted: - # Asset deleted - try: - cls.assets_snapshot.pop(asset_id) - except KeyError: - pass - else: - if asset_id not in cls.assets_snapshot and not cls.first_shot: - cls.assets_snapshot = {asset_id:None,**cls.assets_snapshot} - cls.asset_queue.put(asset_id) # Add asset_id to the queue - cls.asset_set.add(asset_id) # Add asset_id to the set - cls.first_shot = False - cls.block = False # Remove the block to end the thread - - - - - - +class AssetSnapshot(StreamedIdentifiersCache, + api_call=AssetApi(PiecesSettings.api_client).asset_snapshot): + pass def tabulate_from_markdown(md_text): # Split the markdown text into lines diff --git a/main.py b/main.py index 38074b0..7515ba8 100644 --- a/main.py +++ b/main.py @@ -33,7 +33,7 @@ def startup(settings_model): # WEBSOCKETS: # Assets Identifiers Websocket - AssetsIdentifiersWS(AssetSnapshot.assets_snapshot_callback).start() # Load the assets ws at the startup + AssetsIdentifiersWS(AssetSnapshot.streamed_identifiers_callback).start() # Load the assets ws at the startup # User Weboscket PiecesSettings.create_auth_output_panel() diff --git a/search/search_command.py b/search/search_command.py index 12214fa..76a59cd 100644 --- a/search/search_command.py +++ b/search/search_command.py @@ -85,7 +85,7 @@ def search(search_type,query)-> list: # Print the combined asset details if combined_ids: - return {id:AssetSnapshot.assets_snapshot.get(id) for id in combined_ids if AssetSnapshot.assets_snapshot.get(id)} + return {id:identifiers_snapshot.get(id) for id in combined_ids if identifiers_snapshot.get(id)} def is_enabled(self): return PiecesSettings().is_loaded diff --git a/streamed_identifiers.py b/streamed_identifiers.py new file mode 100644 index 0000000..bfbd5ca --- /dev/null +++ b/streamed_identifiers.py @@ -0,0 +1,66 @@ +import queue +from typing import Dict,TypeVar +from pieces_os_client import StreamedIdentifiers +import sublime + +# Define a type variable that matches the return type of api_call +T = TypeVar('T') + + +class StreamedIdentifiersCache: + """ + This class is made for caching Streamed Identifiers. + Please use this class only as a parent class + """ + + def __init_subclass__(cls, api_call,**kwargs): + super().__init_subclass__(**kwargs) + cls.identifiers_snapshot: Dict[str, T] = {} # Map id:return from the api_call + cls.identifiers_queue = queue.Queue() # Queue for asset_ids to be processed + cls.identifiers_set = set() # Set for asset_ids in the queue + cls.block = True # to wait for the queue to receive the first asset id + cls.first_shot = True # First time to open the websocket or not + + + @classmethod + def worker(cls): + try: + while True: + id = cls.identifiers_queue.get(block=cls.block,timeout=5) + cls.identifiers_set.remove(id) # Remove the id from the set + cls.update_identifier(id) + cls.identifiers_queue.task_done() + except queue.Empty: # queue is empty and the block is false + if cls.block: + cls.worker() # if there is more assets to load + return # End the worker + + + @classmethod + def update_identifier(cls,id) -> T: + id_value = api_call(id) + cls.identifiers_snapshot[id] = id_value + return id_value + + @classmethod + def streamed_identifiers_callback(cls,ids:StreamedIdentifiers): + # Start the worker thread if it's not running + cls.block = True + sublime.set_timeout_async(cls.worker) + for item in ids.iterable: + asset_id = item.asset.id + if asset_id not in cls.identifiers_set: + if item.deleted: + # Asset deleted + try: + cls.assets_snapshot.pop(asset_id) + except KeyError: + pass + else: + if asset_id not in cls.assets_snapshot and not cls.first_shot: + cls.assets_snapshot = {asset_id:None,**cls.assets_snapshot} + cls.identifiers_queue.put(asset_id) # Add asset_id to the queue + cls.identifiers_set.add(asset_id) # Add asset_id to the set + cls.first_shot = False + cls.block = False # Remove the block to end the thread + From 5fd167e4a360d7ff3e1c23a184fd559d2edad1f5 Mon Sep 17 00:00:00 2001 From: Bishoy-at-pieces Date: Wed, 12 Jun 2024 15:10:38 +0300 Subject: [PATCH 13/30] fix the streamed identifiers --- streamed_identifiers.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/streamed_identifiers.py b/streamed_identifiers.py index bfbd5ca..e35ffb2 100644 --- a/streamed_identifiers.py +++ b/streamed_identifiers.py @@ -16,9 +16,10 @@ class StreamedIdentifiersCache: def __init_subclass__(cls, api_call,**kwargs): super().__init_subclass__(**kwargs) cls.identifiers_snapshot: Dict[str, T] = {} # Map id:return from the api_call - cls.identifiers_queue = queue.Queue() # Queue for asset_ids to be processed - cls.identifiers_set = set() # Set for asset_ids in the queue - cls.block = True # to wait for the queue to receive the first asset id + cls.identifiers_queue = queue.Queue() # Queue for ids to be processed + cls.identifiers_set = set() # Set for ids in the queue + cls.api_call = api_call + cls.block = True # to wait for the queue to receive the first id cls.first_shot = True # First time to open the websocket or not @@ -32,7 +33,7 @@ def worker(cls): cls.identifiers_queue.task_done() except queue.Empty: # queue is empty and the block is false if cls.block: - cls.worker() # if there is more assets to load + cls.worker() # if there is more ids to load return # End the worker @@ -48,19 +49,19 @@ def streamed_identifiers_callback(cls,ids:StreamedIdentifiers): cls.block = True sublime.set_timeout_async(cls.worker) for item in ids.iterable: - asset_id = item.asset.id - if asset_id not in cls.identifiers_set: + reference_item = getattr(item,"asset",item.conversation) # Get either the conversation or the asset + if id not in cls.identifiers_set: if item.deleted: # Asset deleted try: - cls.assets_snapshot.pop(asset_id) + cls.identifiers_snapshot.pop(id) except KeyError: pass else: - if asset_id not in cls.assets_snapshot and not cls.first_shot: - cls.assets_snapshot = {asset_id:None,**cls.assets_snapshot} - cls.identifiers_queue.put(asset_id) # Add asset_id to the queue - cls.identifiers_set.add(asset_id) # Add asset_id to the set + if id not in cls.identifiers_snapshot and not cls.first_shot: + cls.identifiers_snapshot = {id:None,**cls.identifiers_snapshot} + cls.identifiers_queue.put(id) # Add id to the queue + cls.identifiers_set.add(id) # Add id to the set cls.first_shot = False cls.block = False # Remove the block to end the thread From 0376aedbafb48f8d0f9301d8a051bcf94bdb64dd Mon Sep 17 00:00:00 2001 From: Bishoy-at-pieces Date: Wed, 12 Jun 2024 15:27:24 +0300 Subject: [PATCH 14/30] fix missing class name --- assets/list_assets.py | 4 +- assets/markdown_handler.py | 2 +- search/search_command.py | 1 + streamed_identifiers.py | 141 ++++++++++++++++++++++--------------- 4 files changed, 87 insertions(+), 61 deletions(-) diff --git a/assets/list_assets.py b/assets/list_assets.py index 2d38dfa..1b84a0b 100644 --- a/assets/list_assets.py +++ b/assets/list_assets.py @@ -33,7 +33,7 @@ def run_async(self,pieces_asset_id): # Find all code blocks code_block = re.findall(code_block_pattern, markdown_text) try: - language = identifiers_snapshot[pieces_asset_id].original.reference.classification.specific + language = AssetSnapshot.identifiers_snapshot[pieces_asset_id].original.reference.classification.specific except: language = None PiecesListAssetsCommand.sheets_md[sheet_id] = {"code":"\n".join(code_block[0].split("\n")[1:-1]),"name":api_response.name,"language":language,"id":pieces_asset_id} @@ -51,7 +51,7 @@ def is_enabled(self): class PiecesAssetIdInputHandler(sublime_plugin.ListInputHandler): def list_items(self): - return self.get_assets_list(identifiers_snapshot) + return self.get_assets_list(AssetSnapshot.identifiers_snapshot) def get_assets_list(self,assets_snapshot): assets_list = [] diff --git a/assets/markdown_handler.py b/assets/markdown_handler.py index db37c64..a7e18eb 100644 --- a/assets/markdown_handler.py +++ b/assets/markdown_handler.py @@ -47,7 +47,7 @@ def handle_save(self): if view: asset_id = PiecesHandleMarkdownCommand.views_to_handle.get(view.id()) if asset_id: - asset = identifiers_snapshot[asset_id] + asset = AssetSnapshot.identifiers_snapshot[asset_id] format_api = FormatApi(PiecesSettings.api_client) original = format_api.format_snapshot(asset.original.id, transferable=True) if original.classification.generic == ClassificationGenericEnum.IMAGE: diff --git a/search/search_command.py b/search/search_command.py index 76a59cd..8327b48 100644 --- a/search/search_command.py +++ b/search/search_command.py @@ -85,6 +85,7 @@ def search(search_type,query)-> list: # Print the combined asset details if combined_ids: + identifiers_snapshot = AssetSnapshot.identifiers_snapshot return {id:identifiers_snapshot.get(id) for id in combined_ids if identifiers_snapshot.get(id)} def is_enabled(self): diff --git a/streamed_identifiers.py b/streamed_identifiers.py index e35ffb2..762fcf8 100644 --- a/streamed_identifiers.py +++ b/streamed_identifiers.py @@ -1,67 +1,92 @@ -import queue -from typing import Dict,TypeVar -from pieces_os_client import StreamedIdentifiers -import sublime +""" +A class for caching Streamed Identifiers. This class is designed to be inherited. -# Define a type variable that matches the return type of api_call -T = TypeVar('T') +Attributes: + identifiers_snapshot (Dict[str, Union[Asset, Conversation]]): A dictionary mapping IDs to their corresponding API call results. + identifiers_queue (queue.Queue): A queue for IDs to be processed. + identifiers_set (set): A set for IDs currently in the queue. + api_call (Callable[[str], Union[Asset, Conversation]]): A callable that takes an ID and returns either an Asset or a Conversation. + block (bool): A flag to indicate whether to wait for the queue to receive the first ID. + first_shot (bool): A flag to indicate if it's the first time to open the websocket. + lock (threading.Lock): A lock for thread safety. + worker_thread (threading.Thread): A thread for processing the queue. +Methods: + worker(): Continuously processes IDs from the queue and updates the identifiers_snapshot. + update_identifier(id: str): Updates the identifier snapshot with the result of the API call. + streamed_identifiers_callback(ids: StreamedIdentifiers): Callback method to handle streamed identifiers. -class StreamedIdentifiersCache: - """ - This class is made for caching Streamed Identifiers. - Please use this class only as a parent class - """ +Example: + class AssetSnapshot(StreamedIdentifiersCache,api_call=AssetApi(PiecesSettings.api_client).asset_snapshot): + pass +""" + +import queue +import threading +from typing import Dict, Union, Callable +from pieces_os_client import Conversation, StreamedIdentifiers, Asset +import sublime - def __init_subclass__(cls, api_call,**kwargs): - super().__init_subclass__(**kwargs) - cls.identifiers_snapshot: Dict[str, T] = {} # Map id:return from the api_call - cls.identifiers_queue = queue.Queue() # Queue for ids to be processed - cls.identifiers_set = set() # Set for ids in the queue - cls.api_call = api_call - cls.block = True # to wait for the queue to receive the first id - cls.first_shot = True # First time to open the websocket or not +class StreamedIdentifiersCache: + """ + This class is made for caching Streamed Identifiers. + Please use this class only as a parent class. + """ - @classmethod - def worker(cls): - try: - while True: - id = cls.identifiers_queue.get(block=cls.block,timeout=5) - cls.identifiers_set.remove(id) # Remove the id from the set - cls.update_identifier(id) - cls.identifiers_queue.task_done() - except queue.Empty: # queue is empty and the block is false - if cls.block: - cls.worker() # if there is more ids to load - return # End the worker + def __init_subclass__(cls, api_call: Callable[[str], Union[Asset, Conversation]], **kwargs): + super().__init_subclass__(**kwargs) + cls.identifiers_snapshot: Dict[str, Union[Asset, Conversation,None]] = {} # Map id:return from the api_call + cls.identifiers_queue = queue.Queue() # Queue for ids to be processed + cls.identifiers_set = set() # Set for ids in the queue + cls.api_call = api_call + cls.block = True # to wait for the queue to receive the first id + cls.first_shot = True # First time to open the websocket or not + cls.lock = threading.Lock() # Lock for thread safety + cls.worker_thread = threading.Thread(target=cls.worker, daemon=True) + cls.worker_thread.start() - - @classmethod - def update_identifier(cls,id) -> T: - id_value = api_call(id) - cls.identifiers_snapshot[id] = id_value - return id_value + @classmethod + def worker(cls): + while True: + try: + id = cls.identifiers_queue.get(block=cls.block, timeout=5) + with cls.lock: + cls.identifiers_set.remove(id) # Remove the id from the set + cls.update_identifier(id) + cls.identifiers_queue.task_done() + except queue.Empty: # queue is empty and the block is false + if cls.block: + continue # if there are more ids to load + return # End the worker - @classmethod - def streamed_identifiers_callback(cls,ids:StreamedIdentifiers): - # Start the worker thread if it's not running - cls.block = True - sublime.set_timeout_async(cls.worker) - for item in ids.iterable: - reference_item = getattr(item,"asset",item.conversation) # Get either the conversation or the asset - if id not in cls.identifiers_set: - if item.deleted: - # Asset deleted - try: - cls.identifiers_snapshot.pop(id) - except KeyError: - pass - else: - if id not in cls.identifiers_snapshot and not cls.first_shot: - cls.identifiers_snapshot = {id:None,**cls.identifiers_snapshot} - cls.identifiers_queue.put(id) # Add id to the queue - cls.identifiers_set.add(id) # Add id to the set - cls.first_shot = False - cls.block = False # Remove the block to end the thread + @classmethod + def update_identifier(cls, id: str): + try: + id_value = cls.api_call(id) + with cls.lock: + cls.identifiers_snapshot[id] = id_value + return id_value + except: + return None + @classmethod + def streamed_identifiers_callback(cls, ids: StreamedIdentifiers): + # Start the worker thread if it's not running + cls.block = True + sublime.set_timeout_async(cls.worker) + for item in ids.iterable: + reference_item = getattr(item, "asset", item.conversation) # Get either the conversation or the asset + id = reference_item.id + with cls.lock: + if id not in cls.identifiers_set: + if item.deleted: + # Asset deleted + cls.identifiers_snapshot.pop(id, None) + else: + if id not in cls.identifiers_snapshot and not cls.first_shot: + cls.identifiers_snapshot = {id: None, **cls.identifiers_snapshot} + cls.identifiers_queue.put(id) # Add id to the queue + cls.identifiers_set.add(id) # Add id to the set + cls.first_shot = False + cls.block = False # Remove the block to end the thread \ No newline at end of file From 7c026d64bf54449f21396c013c15056ff30adca2 Mon Sep 17 00:00:00 2001 From: Bishoy-at-pieces Date: Wed, 12 Jun 2024 16:56:15 +0300 Subject: [PATCH 15/30] add the conversations --- copilot/__init__.py | 4 ++- copilot/ask_command.py | 36 ++++++++++++++++++++-- copilot/conversation_websocket.py | 17 +++++++++++ copilot/conversations.py | 12 ++++++++ main.py | 2 ++ settings.py | 5 ++- streamed_identifiers.py | 51 ++++++++++++++++++------------- 7 files changed, 98 insertions(+), 29 deletions(-) create mode 100644 copilot/conversation_websocket.py create mode 100644 copilot/conversations.py diff --git a/copilot/__init__.py b/copilot/__init__.py index 6857ad2..613aca8 100644 --- a/copilot/__init__.py +++ b/copilot/__init__.py @@ -1 +1,3 @@ -from .ask_command import PiecesAskStreamCommand,PiecesEnterResponseCommand \ No newline at end of file +from .ask_command import PiecesAskStreamCommand,PiecesEnterResponseCommand +from .conversation_websocket import ConversationWS +from .conversations import ConversationsSnapshot \ No newline at end of file diff --git a/copilot/ask_command.py b/copilot/ask_command.py index 726d64a..cb55205 100644 --- a/copilot/ask_command.py +++ b/copilot/ask_command.py @@ -1,16 +1,23 @@ +from pieces_os_client import AnnotationApi import sublime_plugin +import sublime from .ask_view import CopilotViewManager +from .conversations import ConversationsSnapshot from ..settings import PiecesSettings copilot = CopilotViewManager() class PiecesAskStreamCommand(sublime_plugin.WindowCommand): - def run(self): + def run(self,pieces_conversation_id=None): copilot.ask_websocket.start() + copilot.conversation_id = pieces_conversation_id # Set the conversation self.window.focus_view(copilot.gpt_view) return - + + def input(self,args): + return PiecesConversationIdInputHandler() + def is_enabled(self): return PiecesSettings().is_loaded @@ -20,4 +27,27 @@ def run(self,_): copilot.ask() def is_enabled(self): - return PiecesSettings().is_loaded \ No newline at end of file + return PiecesSettings().is_loaded + + + +class PiecesConversationIdInputHandler(sublime_plugin.ListInputHandler): + def list_items(self): + conversation_list = [ + sublime.ListInputItem(text="Create New Conversation", value=None) + ] + api = AnnotationApi(PiecesSettings.api_client) + for conversation in ConversationsSnapshot.identifiers_snapshot.values(): + name = getattr(conversation,"name","New Conversation") + annotations = conversation.annotations.indices + details = None + if annotations: + details = api.annotation_specific_annotation_snapshot(list(annotations.keys())[0]).text + conversation_list.append(sublime.ListInputItem(text=name, value=conversation.id,details=details)) + + + return conversation_list + + def placeholder(self): + return "Choose an asset" + diff --git a/copilot/conversation_websocket.py b/copilot/conversation_websocket.py new file mode 100644 index 0000000..02152d2 --- /dev/null +++ b/copilot/conversation_websocket.py @@ -0,0 +1,17 @@ +from pieces_os_client import StreamedIdentifiers + +from ..settings import PiecesSettings +from ..base_websocket import BaseWebsocket + +class ConversationWS(BaseWebsocket): + def __new__(cls,*args,**kwargs): + if not hasattr(cls, 'instance'): + cls.instance = super(ConversationWS, cls).__new__(cls) + return cls.instance + + @property + def url(self): + return PiecesSettings.CONVERSATION_WS_URL + + def on_message(self,ws, message): + self.on_message_callback(StreamedIdentifiers.from_json(message)) diff --git a/copilot/conversations.py b/copilot/conversations.py new file mode 100644 index 0000000..4e0b360 --- /dev/null +++ b/copilot/conversations.py @@ -0,0 +1,12 @@ +from ..streamed_identifiers import StreamedIdentifiersCache +from ..settings import PiecesSettings +from pieces_os_client import ConversationApi + +class ConversationsSnapshot(StreamedIdentifiersCache, + api_call=ConversationApi(PiecesSettings.api_client).conversation_get_specific_conversation): + + @classmethod + def sort_first_shot(cls): + # Sort the dictionary by the "updated" timestamp + sorted_conversations = sorted(cls.identifiers_snapshot.values(), key=lambda x: x.updated.value, reverse=True) + cls.identifiers_snapshot = {conversation.id:conversation for conversation in sorted_conversations} \ No newline at end of file diff --git a/main.py b/main.py index 7515ba8..35ce93d 100644 --- a/main.py +++ b/main.py @@ -39,6 +39,8 @@ def startup(settings_model): PiecesSettings.create_auth_output_panel() AuthWebsocket(AuthUser.on_user_callback).start() # Load the stream user websocket + # Conversation Websocket + ConversationWS(ConversationsSnapshot.streamed_identifiers_callback).start() def plugin_loaded(): diff --git a/settings.py b/settings.py index 92b548b..eff7051 100644 --- a/settings.py +++ b/settings.py @@ -69,13 +69,12 @@ def host_init(cls,host): else: cls.host = "http://127.0.0.1:1000" + # Websocket urls ws_base_url = cls.host.replace('http','ws') - cls.ASSETS_IDENTIFIERS_WS_URL = ws_base_url + "/assets/stream/identifiers" - cls.AUTH_WS_URL = ws_base_url + "/user/stream" - cls.ASK_STREAM_WS_URL = ws_base_url + "/qgpt/stream" + cls.CONVERSATION_WS_URL = ws_base_url + "/conversations/stream/identifiers" configuration = pos_client.Configuration(host=cls.host) diff --git a/streamed_identifiers.py b/streamed_identifiers.py index 762fcf8..c6e0238 100644 --- a/streamed_identifiers.py +++ b/streamed_identifiers.py @@ -42,30 +42,38 @@ def __init_subclass__(cls, api_call: Callable[[str], Union[Asset, Conversation]] cls.api_call = api_call cls.block = True # to wait for the queue to receive the first id cls.first_shot = True # First time to open the websocket or not - cls.lock = threading.Lock() # Lock for thread safety - cls.worker_thread = threading.Thread(target=cls.worker, daemon=True) - cls.worker_thread.start() + + + @classmethod + def sort_first_shot(cls): + """ + Sorting algrothim in the first shot + """ + pass @classmethod def worker(cls): while True: try: id = cls.identifiers_queue.get(block=cls.block, timeout=5) - with cls.lock: - cls.identifiers_set.remove(id) # Remove the id from the set + cls.identifiers_set.remove(id) # Remove the id from the set cls.update_identifier(id) cls.identifiers_queue.task_done() except queue.Empty: # queue is empty and the block is false if cls.block: continue # if there are more ids to load + + if cls.first_shot: + cls.first_shot = False + cls.sort_first_shot() + return # End the worker @classmethod - def update_identifier(cls, id: str): + def update_identifier(cls, identifier: str): try: - id_value = cls.api_call(id) - with cls.lock: - cls.identifiers_snapshot[id] = id_value + id_value = cls.api_call(identifier) + cls.identifiers_snapshot[identifier] = id_value return id_value except: return None @@ -76,17 +84,16 @@ def streamed_identifiers_callback(cls, ids: StreamedIdentifiers): cls.block = True sublime.set_timeout_async(cls.worker) for item in ids.iterable: - reference_item = getattr(item, "asset", item.conversation) # Get either the conversation or the asset - id = reference_item.id - with cls.lock: - if id not in cls.identifiers_set: - if item.deleted: - # Asset deleted - cls.identifiers_snapshot.pop(id, None) - else: - if id not in cls.identifiers_snapshot and not cls.first_shot: - cls.identifiers_snapshot = {id: None, **cls.identifiers_snapshot} - cls.identifiers_queue.put(id) # Add id to the queue - cls.identifiers_set.add(id) # Add id to the set - cls.first_shot = False + reference_id = item.asset.id if item.asset else item.conversation.id # Get either the conversation or the asset + + if reference_id not in cls.identifiers_set: + if item.deleted: + # Asset deleted + cls.identifiers_snapshot.pop(reference_id, None) + else: + if reference_id not in cls.identifiers_snapshot and not cls.first_shot: + cls.identifiers_snapshot = {reference_id: None, **cls.identifiers_snapshot} + cls.identifiers_queue.put(reference_id) # Add id to the queue + cls.identifiers_set.add(reference_id) # Add id to the set + cls.block = False # Remove the block to end the thread \ No newline at end of file From d1934ae6e8ca98e7bef7d0af032c4f0f6ac22c31 Mon Sep 17 00:00:00 2001 From: Bishoy-at-pieces Date: Wed, 12 Jun 2024 18:40:13 +0300 Subject: [PATCH 16/30] fix rendering and paste events --- copilot/ask_command.py | 16 +++++++----- copilot/ask_view.py | 56 +++++++++++++++++++++++++++++++++++++----- event_listener.py | 2 ++ 3 files changed, 62 insertions(+), 12 deletions(-) diff --git a/copilot/ask_command.py b/copilot/ask_command.py index cb55205..edb56a1 100644 --- a/copilot/ask_command.py +++ b/copilot/ask_command.py @@ -11,8 +11,8 @@ class PiecesAskStreamCommand(sublime_plugin.WindowCommand): def run(self,pieces_conversation_id=None): copilot.ask_websocket.start() - copilot.conversation_id = pieces_conversation_id # Set the conversation self.window.focus_view(copilot.gpt_view) + copilot.render_conversation(pieces_conversation_id) return def input(self,args): @@ -39,13 +39,17 @@ def list_items(self): api = AnnotationApi(PiecesSettings.api_client) for conversation in ConversationsSnapshot.identifiers_snapshot.values(): name = getattr(conversation,"name","New Conversation") - annotations = conversation.annotations.indices - details = None - if annotations: - details = api.annotation_specific_annotation_snapshot(list(annotations.keys())[0]).text - conversation_list.append(sublime.ListInputItem(text=name, value=conversation.id,details=details)) + if not name: name = "New Conversation" + + try: + id = list(conversation.annotations.indices.keys())[0] + details = str(api.annotation_specific_annotation_snapshot(id).text).replace("\n"," ") + except AttributeError: + details = "" + conversation_list.append(sublime.ListInputItem(text=name, value=conversation.id,details=details)) + return conversation_list def placeholder(self): diff --git a/copilot/ask_view.py b/copilot/ask_view.py index 8c5bfe6..e890887 100644 --- a/copilot/ask_view.py +++ b/copilot/ask_view.py @@ -1,6 +1,7 @@ import sublime from .ask_websocket import AskStreamWS -from pieces_os_client import QGPTQuestionInput, QGPTStreamInput, RelevantQGPTSeeds,QGPTStreamOutput +from .conversations import ConversationsSnapshot +from pieces_os_client import ConversationMessageApi, QGPTQuestionInput, QGPTStreamInput, RelevantQGPTSeeds,QGPTStreamOutput from ..settings import PiecesSettings from sublime import Region import re @@ -21,7 +22,6 @@ class CopilotViewManager: @property def gpt_view(self) -> sublime.View: if not getattr(CopilotViewManager, "_gpt_view",None): - # File config and creation CopilotViewManager._gpt_view = sublime.active_window().new_file(syntax="Packages/Markdown/Markdown.sublime-syntax") CopilotViewManager.can_type = True @@ -31,8 +31,8 @@ def gpt_view(self) -> sublime.View: CopilotViewManager._gpt_view.set_name("Pieces Copilot") # Phantom intilization - self.phantom_set = sublime.PhantomSet(CopilotViewManager._gpt_view, "Pieces_Phantoms") self.last_edit_phantom = 0 + self.phantom_set = sublime.PhantomSet(CopilotViewManager._gpt_view, "Pieces_Phantoms") self.code_blocks_dict = {} # id: code @@ -55,13 +55,16 @@ def update_status_bar(self): def show_cursor(self): self.gpt_view.set_status("MODEL",PiecesSettings.model_name) self.gpt_view.run_command("append",{"characters":">>> "}) - self.gpt_view.settings().set("end_response",self.end_response+4) # ">>> " 4 characters + self.end_response = self.end_response + 4 # ">>> " 4 characters self.select_end @property def end_response(self) -> int: return self.gpt_view.settings().get("end_response") + @end_response.setter + def end_response(self,e): + self.gpt_view.settings().set("end_response",e) def on_message_callback(self,message: QGPTStreamOutput): if message.question: @@ -72,7 +75,7 @@ def on_message_callback(self,message: QGPTStreamOutput): if message.status == "COMPLETED": self.new_line() - self.gpt_view.settings().set("end_response",self.gpt_view.size()) # Update the size + self.end_response = self.gpt_view.size() # Update the size self.show_cursor CopilotViewManager.can_type = True self.conversation_id = message.conversation @@ -136,7 +139,7 @@ def add_code_phantoms(self): self.code_blocks_dict[id] = match.group(1) end_point = match.end() phantom = sublime.Phantom( - sublime.Region(end_point, end_point), + sublime.Region(end_point+self.last_edit_phantom, end_point+self.last_edit_phantom), PHANTOM_CONTENT.format(id), sublime.LAYOUT_BELOW, on_navigate=self.on_nav @@ -156,3 +159,44 @@ def on_nav(self,href:str): elif command == "copy": sublime.set_clipboard(code) + + def render_conversation(self,conversation_id): + self.conversation_id = conversation_id # Set the conversation + + # Clear everything! + self._gpt_view = None # clear the old _gpt_view + + + + if conversation_id: + conversation = ConversationsSnapshot.identifiers_snapshot.get(conversation_id) + if not conversation: + return sublime.error_message("Conversation not found") # Error conversation not found + else: + return # Nothing need to be rendered + + self.gpt_view.run_command("select_all") + self.gpt_view.run_command("right_delete") # Clear the cursor created by default ">>>" + + message_api = ConversationMessageApi(PiecesSettings.api_client) + first_message = True + for key,val in conversation.messages.indices.items(): + if val == -1: # message is deleted + continue + message = message_api.message_specific_message_snapshot(message=key) + if message.role == "USER": + if not first_message: + self.new_line() + first_message = False + + self.show_cursor + + + if message.fragment.string: + self.gpt_view.run_command("append",{"characters":message.fragment.string.raw}) + + + self.new_line() + self.show_cursor + self.end_response = self.gpt_view.size() + self.add_code_phantoms() \ No newline at end of file diff --git a/event_listener.py b/event_listener.py index 147aa75..49acea2 100644 --- a/event_listener.py +++ b/event_listener.py @@ -16,6 +16,8 @@ def on_window_command(self, window, command_name, args): def on_text_command(self,view,command_name,args): self.check(command_name) + if command_name == "paste": # To avoid pasting in the middle of the view of the copilot + self.on_query_context(view,"pieces_copilot_add",True,sublime.OP_EQUAL,True) def check(self,command_name): if command_name.startswith("pieces_") and command_name not in PiecesEventListener.commands_to_exclude: # Check any command From 9748392d1ebc324d8c2541878547b64c24c54ab2 Mon Sep 17 00:00:00 2001 From: Bishoy-at-pieces Date: Wed, 12 Jun 2024 19:35:48 +0300 Subject: [PATCH 17/30] add phantom stats on click --- copilot/ask_view.py | 51 +++++++++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/copilot/ask_view.py b/copilot/ask_view.py index e890887..9e2b442 100644 --- a/copilot/ask_view.py +++ b/copilot/ask_view.py @@ -11,8 +11,8 @@ PHANTOM_CONTENT = f""" """ @@ -33,7 +33,7 @@ def gpt_view(self) -> sublime.View: # Phantom intilization self.last_edit_phantom = 0 self.phantom_set = sublime.PhantomSet(CopilotViewManager._gpt_view, "Pieces_Phantoms") - self.code_blocks_dict = {} # id: code + self.phantom_details_dict = {} # id: {"code":code,"region":region} # Others @@ -79,7 +79,7 @@ def on_message_callback(self,message: QGPTStreamOutput): self.show_cursor CopilotViewManager.can_type = True self.conversation_id = message.conversation - self.add_code_phantoms() # Generate the code phantoms + self.add_code_phantoms() # Generate the code phantoms @property def conversation_id(self): return self.gpt_view.settings().get("conversation_id") @@ -129,35 +129,46 @@ def add_code_phantoms(self): # Regular expression to find code blocks in Markdown code_block_pattern = re.compile(r'```.*?\n(.*?)```', re.DOTALL) matches = code_block_pattern.finditer(content) - - - phantoms = [] + + if not matches: + return # No matches found in this region for match in matches: - id = str(len(self.code_blocks_dict)) + id = str(len(self.phantom_details_dict)) # Create a phantom at the end of each code block - self.code_blocks_dict[id] = match.group(1) - end_point = match.end() - phantom = sublime.Phantom( - sublime.Region(end_point+self.last_edit_phantom, end_point+self.last_edit_phantom), - PHANTOM_CONTENT.format(id), - sublime.LAYOUT_BELOW, - on_navigate=self.on_nav - ) - phantoms.append(phantom) - self.phantom_set.update(phantoms) + + end_point = match.end() + region = sublime.Region(end_point+self.last_edit_phantom, end_point+self.last_edit_phantom) + self.phantom_details_dict[id] = {"code":match.group(1),"region":region} + self.update_phantom_set(region,id) self.last_edit_phantom = view.size() def on_nav(self,href:str): command,id = href.split("_") - code = self.code_blocks_dict[id] - + code = self.phantom_details_dict[id]["code"] + region = self.phantom_details_dict[id]["region"] if command == "save": self.gpt_view.run_command("pieces_create_asset",{"data":code}) + self.update_phantom_set(region,id,"Saving") + sublime.set_timeout_async(lambda:self.update_phantom_set(region,id,"Saved"),5000) + elif command == "copy": sublime.set_clipboard(code) + self.update_phantom_set(region,id,copy="Copied") + sublime.set_timeout_async(lambda:self.update_phantom_set(region,id,copy="Copy"),5000) + + + def update_phantom_set(self,region,id,save="Save",copy="Copy"): + # Change the text + phantom = sublime.Phantom( + region, + PHANTOM_CONTENT.format(id = id,copy=copy,save=save), + sublime.LAYOUT_BELOW, + on_navigate=self.on_nav + ) + self.phantom_set.update([phantom]) def render_conversation(self,conversation_id): From ffb69be951fa23e3d11b72880c780d26c8641b5b Mon Sep 17 00:00:00 2001 From: Bishoy-at-pieces Date: Wed, 12 Jun 2024 19:57:53 +0300 Subject: [PATCH 18/30] fix some bugs --- copilot/ask_view.py | 19 +++++++++---------- event_listener.py | 10 +++++++++- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/copilot/ask_view.py b/copilot/ask_view.py index 9e2b442..0249d6d 100644 --- a/copilot/ask_view.py +++ b/copilot/ask_view.py @@ -79,7 +79,7 @@ def on_message_callback(self,message: QGPTStreamOutput): self.show_cursor CopilotViewManager.can_type = True self.conversation_id = message.conversation - self.add_code_phantoms() # Generate the code phantoms + self.add_code_phantoms() # Generate the code phantoms @property def conversation_id(self): return self.gpt_view.settings().get("conversation_id") @@ -98,13 +98,16 @@ def new_line(self,lines = 2) -> None: self.gpt_view.run_command("append",{"characters":"\n"}) def ask(self,relevant=RelevantQGPTSeeds(iterable=[])): + query = self.gpt_view.substr(Region(self.end_response,self.gpt_view.size())) + if not query: + return CopilotViewManager.can_type = False self.select_end # got to the end of the text to enter the new lines self.new_line() self.ask_websocket.send_message( QGPTStreamInput( question=QGPTQuestionInput( - query = self.gpt_view.substr(Region(self.end_response,self.gpt_view.size())), + query=query, relevant = relevant, application=PiecesSettings.get_application().id, model = PiecesSettings.model_id @@ -130,8 +133,6 @@ def add_code_phantoms(self): code_block_pattern = re.compile(r'```.*?\n(.*?)```', re.DOTALL) matches = code_block_pattern.finditer(content) - if not matches: - return # No matches found in this region for match in matches: id = str(len(self.phantom_details_dict)) @@ -196,16 +197,14 @@ def render_conversation(self,conversation_id): continue message = message_api.message_specific_message_snapshot(message=key) if message.role == "USER": - if not first_message: - self.new_line() - first_message = False - self.show_cursor - - + if message.fragment.string: self.gpt_view.run_command("append",{"characters":message.fragment.string.raw}) + if not first_message: + self.new_line() + first_message = False self.new_line() self.show_cursor diff --git a/event_listener.py b/event_listener.py index 49acea2..282adda 100644 --- a/event_listener.py +++ b/event_listener.py @@ -72,7 +72,15 @@ def on_query_context(self,view,key, operator, operand, match_all): def on_init(self,views): for view in views: if view.settings().get("PIECES_GPT_VIEW"): - copilot.gpt_view = view # Set the old views + # Update the conversation to be real-time + # Close the old view and rerender the conversation + conversation = view.settings().get("conversation_id") + if conversation: + view.close(lambda x: copilot.render_conversation(conversation)) + + + + class PiecesViewEventListener(sublime_plugin.ViewEventListener): def on_close(self): From 667ec0fbc1b83a48e5caf20610c3377ca9936c60 Mon Sep 17 00:00:00 2001 From: Bishoy-at-pieces Date: Thu, 13 Jun 2024 22:21:56 +0300 Subject: [PATCH 19/30] fix buttons and added the split view --- Pieces.sublime-commands | 2 +- copilot/ask_command.py | 36 ++++++++--- copilot/ask_view.py | 84 +++++++++++++++++++++----- copilot/images/copilot-icon-dark.png | Bin 0 -> 1626 bytes copilot/images/copilot-icon-light.png | Bin 0 -> 1132 bytes event_listener.py | 4 +- 6 files changed, 103 insertions(+), 23 deletions(-) create mode 100644 copilot/images/copilot-icon-dark.png create mode 100644 copilot/images/copilot-icon-light.png diff --git a/Pieces.sublime-commands b/Pieces.sublime-commands index 2609926..f5a0bca 100644 --- a/Pieces.sublime-commands +++ b/Pieces.sublime-commands @@ -33,7 +33,7 @@ "command": "pieces_reload" }, { - "caption": "Pieces: Ask", + "caption": "Pieces: Copilot", "command": "pieces_ask_stream" } ] \ No newline at end of file diff --git a/copilot/ask_command.py b/copilot/ask_command.py index edb56a1..234cb95 100644 --- a/copilot/ask_command.py +++ b/copilot/ask_command.py @@ -9,18 +9,42 @@ class PiecesAskStreamCommand(sublime_plugin.WindowCommand): - def run(self,pieces_conversation_id=None): + def run(self,pieces_choose_type,pieces_query=None,pieces_conversation_id=None): copilot.ask_websocket.start() - self.window.focus_view(copilot.gpt_view) copilot.render_conversation(pieces_conversation_id) + if pieces_query: + copilot.gpt_view.run_command("append",{"charaters":pieces_queryR}) + copilot.gpt_view.run_command("pieces_enter_response") return def input(self,args): - return PiecesConversationIdInputHandler() + return PiecesChooseTypeInputHandler() def is_enabled(self): return PiecesSettings().is_loaded +class PiecesChooseTypeInputHandler(sublime_plugin.ListInputHandler): + def list_items(self): + return [ + ("Create New Conversation", "new"), + # ("Search a Conversation", "search"), + ("View Conversation List","view"), + ("Ask a question","question") + ] + def next_input(self, args): + t = args["pieces_choose_type"] + if t == "search": + return # TODO: Add searching via endpoint + elif t == "view": + return PiecesConversationIdInputHandler() + elif t == "question": + return PiecesQueryInputHandler() + + +class PiecesQueryInputHandler(sublime_plugin.TextInputHandler): + def placeholder(self) -> str: + return "Enter a query to ask the copilot about" + class PiecesEnterResponseCommand(sublime_plugin.TextCommand): def run(self,_): @@ -33,9 +57,7 @@ def is_enabled(self): class PiecesConversationIdInputHandler(sublime_plugin.ListInputHandler): def list_items(self): - conversation_list = [ - sublime.ListInputItem(text="Create New Conversation", value=None) - ] + conversation_list = [] api = AnnotationApi(PiecesSettings.api_client) for conversation in ConversationsSnapshot.identifiers_snapshot.values(): name = getattr(conversation,"name","New Conversation") @@ -53,5 +75,5 @@ def list_items(self): return conversation_list def placeholder(self): - return "Choose an asset" + return "Choose a conversation or start new one" diff --git a/copilot/ask_view.py b/copilot/ask_view.py index 0249d6d..4464c36 100644 --- a/copilot/ask_view.py +++ b/copilot/ask_view.py @@ -18,17 +18,18 @@ class CopilotViewManager: can_type = True - + @property def gpt_view(self) -> sublime.View: - if not getattr(CopilotViewManager, "_gpt_view",None): + if not getattr(CopilotViewManager, "_gpt_view",None): # TODO: open the copilot in split view # File config and creation CopilotViewManager._gpt_view = sublime.active_window().new_file(syntax="Packages/Markdown/Markdown.sublime-syntax") CopilotViewManager.can_type = True CopilotViewManager._gpt_view.settings().set("PIECES_GPT_VIEW",True) # Label the view as gpt view CopilotViewManager._gpt_view.settings().set("end_response",0) # End reponse charater + CopilotViewManager._gpt_view.settings().set("line_numbers", False) # Remove lines CopilotViewManager._gpt_view.set_scratch(True) - CopilotViewManager._gpt_view.set_name("Pieces Copilot") + CopilotViewManager._gpt_view.set_name(self.view_name) # Phantom intilization self.last_edit_phantom = 0 @@ -37,10 +38,35 @@ def gpt_view(self) -> sublime.View: # Others + self.copilot_regions = [] self.show_cursor self.update_status_bar() + self.render_copilot_image_phantom(CopilotViewManager._gpt_view) + + + # Create a new group (split view) + sublime.active_window().run_command("set_layout", { + "cols": [0.0, 0.5, 1.0], + "rows": [0.0, 1.0], + "cells": [[0, 0, 1, 1], [1, 0, 2, 1]] + }) + + # Move the active view to the new group + sublime.active_window().set_view_index(CopilotViewManager._gpt_view, 1, 0) + + # Focus on the new group + sublime.active_window().focus_group(1) return CopilotViewManager._gpt_view + @property + def view_name(self): + return "Pieces: " + getattr(self,"_view_name","New Conversation") + + @view_name.setter + def view_name(self,v): + self._view_name = v + self.gpt_view.set_name(self.view_name) + @gpt_view.setter def gpt_view(self,view): @@ -56,6 +82,18 @@ def show_cursor(self): self.gpt_view.set_status("MODEL",PiecesSettings.model_name) self.gpt_view.run_command("append",{"characters":">>> "}) self.end_response = self.end_response + 4 # ">>> " 4 characters + ui = sublime.ui_info()["theme"]["style"] + + self.copilot_regions.append(sublime.Region(self.gpt_view.size(), self.gpt_view.size())) + + # Add the regions with the icon and appropriate flags + self.gpt_view.add_regions( + "pieces", + self.copilot_regions, + scope="text", + icon=f"Packages/Pieces/copilot/images/copilot-icon-{ui}.png", + flags=sublime.HIDDEN + ) self.select_end @property @@ -161,7 +199,7 @@ def on_nav(self,href:str): sublime.set_timeout_async(lambda:self.update_phantom_set(region,id,copy="Copy"),5000) - def update_phantom_set(self,region,id,save="Save",copy="Copy"): + def update_phantom_set(self,region,id,save="Save",copy="Copy",reset = False): # Change the text phantom = sublime.Phantom( region, @@ -169,15 +207,33 @@ def update_phantom_set(self,region,id,save="Save",copy="Copy"): sublime.LAYOUT_BELOW, on_navigate=self.on_nav ) - self.phantom_set.update([phantom]) - + + if not reset: + phantoms = [phantom for phantom in self.phantom_set.phantoms if phantom.region != region] + phantoms = [phantom,*phantoms] + else: + phantoms = [phantom] + self.phantom_set.update(phantoms) + + @staticmethod + def render_copilot_image_phantom(view:sublime.View): + pass + # ui = sublime.ui_info()["theme"]["style"] + # view.run_command("append",{"characters":"\n"}) + # view.add_phantom( + # key="Pieces_image", + # region = sublime.Region(0,60), + # layout = sublime.LAYOUT_INLINE, + # content =f"" + # ) def render_conversation(self,conversation_id): + self.conversation_id = conversation_id # Set the conversation # Clear everything! self._gpt_view = None # clear the old _gpt_view - + self.phantom_set.update([]) # Clear old phantoms if conversation_id: @@ -185,14 +241,17 @@ def render_conversation(self,conversation_id): if not conversation: return sublime.error_message("Conversation not found") # Error conversation not found else: - return # Nothing need to be rendered + self.gpt_view # Nothing need to be rendered + if hasattr(self,"_view_name"): delattr(self,"_view_name") + return + self.view_name = conversation.name self.gpt_view.run_command("select_all") self.gpt_view.run_command("right_delete") # Clear the cursor created by default ">>>" - message_api = ConversationMessageApi(PiecesSettings.api_client) - first_message = True + for key,val in conversation.messages.indices.items(): + self.select_end if val == -1: # message is deleted continue message = message_api.message_specific_message_snapshot(message=key) @@ -202,11 +261,8 @@ def render_conversation(self,conversation_id): if message.fragment.string: self.gpt_view.run_command("append",{"characters":message.fragment.string.raw}) - if not first_message: - self.new_line() - first_message = False + self.new_line() - self.new_line() self.show_cursor self.end_response = self.gpt_view.size() self.add_code_phantoms() \ No newline at end of file diff --git a/copilot/images/copilot-icon-dark.png b/copilot/images/copilot-icon-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..db0f3dd8943f6dfbb0e252f0564ab0ac21c271ee GIT binary patch literal 1626 zcmV-g2BrClP))FnpbDsB{=lP!JJm)#* zybd)D6dxadm&4&$KQ=bj3xBe`y}d>SNsiRi)G)W(y%MEYj*gB-D4PV3IgGtmC+el_ zQvndV`kYQ@Wov8eQAa{T!fq6=Q~x6Z;KMF>rm|0?Or{P6r>VzKmij0@%=e4n+3+}J zlNb{%9!sTCK|9q?|5o~aLisE*GV)}7eSMz@iLcoJIEZ|?0H*Bd=;$c+42p}3OM~UI zM9b-O6ZL36b`BO46x{9bv@mcR1`4UhQo<55j*y-#aZWEhoo2 zDjOum2L}f?plE|O^EOHep!aIt6422@LqnU{NE@{Yp2Ax%wzakG_w=zsmVk*d02MJS zF&1gir3i4lq(W$XNWYZ^ea^~+NF|>u3*?;YV{&rxT$Xs-z`#HQ#)?fqsadmT{i18u znW&qOj>G7yWjR-?K!Ayd7+gvm{YG=E@)NN1tM@8&I&0O`l!52o#j_QZJIy*dd;o>v zog%86DN)KA4X}*f#2gzNy9jVUS{y5>;SB7d&d$ywdbv{rBpxauj#_CxV*`}XPx^-D zJ?la4aEJ%LWQk8DCMMqQm1}uSl>5sshJ<&@*l}z1GKnKth6;?TqNAhpoPb&pC@3Mu zf9xv*>^CZK%I@juS>#kf5^FG?USJ4^Nq)QGi&4A>(TD|wJw`${K##IU-Ex29l*lDc%k>}rvy^~qWb3YDKsvMd zz>ng&`T6-SUm?qz;%A(j@@#l`cn0>K<2*g@+rc!(1R%3mg^}w3dBUVyL`_Z2t-eA$ zZk9MDjl7A@407KVE&#nI{gmI}=Qeo)kOJ-wnvK5SC5v-(E%Y==6_4WSjRyUWBVPcZ z2hrK&$EvxuT`=RfSc>a0 z_BzeKb)VvWK>fjzJ`ocWv&=3iQ(zoVxp?5yq@<)PjK0fKV5|^uSv2?Rzaf*0;ugxK zY@!#ctE=5A05V(3GRoy_W4S)xV>Vbe2y6`#sQ}P>YCKa6FX-;>ZdE~&p~o;1DjZ<( z${KmD-TzDnD5cC}OoCgk#4r1P;4UA^GOv}xU(FyHE+qke0CvFWTYrDQJZ|?8*x$jI zs2L*LyOQO-TB|NjOG~?0%@8?t2D7Xz5Ao-@X&&*sEvJLnKucK#??mN3R;?Mliu!55 z?aa!`nr)^)60MwtAt50v)HD$GVu{(WuwO}2fDOqD-$|}HO5;X~@KC@#PK^C38D8E1 zM#{)z=bD?F2UHLwZ&YmTz+RJPpX!)QI$IvelO7|ua}{W}s|&|X153P~j^$Gsk%X)r z^phSYMnuw%7#SInW9@0By)EP%l~aPT-+KB@3Vyt|pO2SOU85!r?0KY&FL3uU$}K7g z%E`$I!9({mQF#(d8j@cF-^&G&@W|7H8V75pNY-9^*tc!tIx-%9{qD1oj0O%hO}=qkl) zDd^sA21>~CPZf-0;r3TBlCO!*QFdi!X1>a0af#kz;%(}i=vx4n9q#7)M*!MNxSX|O z&(_}_Av~}Q_BLgnCThf}{789BfKlH`eUqB_(Px# literal 0 HcmV?d00001 diff --git a/copilot/images/copilot-icon-light.png b/copilot/images/copilot-icon-light.png new file mode 100644 index 0000000000000000000000000000000000000000..e5c6dc7ab5d88874569c49eb44a3c1ddc1b2d199 GIT binary patch literal 1132 zcmV-y1e5!TP)Yg6;hwt{Ss#B*@W%wjiz5$+v523rHS63Y$J)vllbpqzXP^c++;D2Y>U~TTg6t@M^z2G-k1s6Tq zQM-zQunjiy+>4H_mcIta$-iOqbgT*Ki#FYbOEu|JPCae!WB3dP zK^Keq*b8;o?~=6A#$lp!=O^JWy+g;QD@=py)@Hx$u?opswz0`@Ii~wZI<{+KY>vP; zl812gl8zaZJT|^?>dZ*6ZPXr=EV4Z`0nQjd^>XU|=~<@%e)bKh3dTZ<@z=GGkA5bd zvP#)$mwl1>NVD`+ldxPjfF5#4F@hXrs~ zl64_vepu&VG}O}eMz`RRM~T5v&~4D$uu|HVF2I!$`EMlDy9LwX9om`!tzm=MF=hiC z#@|n2M~s0Lt6TLms_h(;gqZ`kWG0T8% zy?ZCLam?Bd^bX9a{C7-%r{Hu%{tg)A>H9%osh117e-W3@VIEv@ih4--ZuLBjg2B{j zkd%rHjEHMt9lR&e!8LTKn7Zs$`VU zqwHj_J;YC?ZTD?@=%nPJxWLB30hk>w;BGpZU_0GYI#5xIqmH3^F7+{W^l+m+>9a4Vt4~)Lgxf0ufuary4Kde zCMiXoZm=a3!z-o2!LZ3GEZP;m%F}u3z)WyPooXJQKkvcn$hDKqVW#|b(*#338u1Pc zn`#(t$`3sH9vzTW@fr9d6rfi8x;=q8evdhR_xRj+*pWW_Ii~!?v)n{XkfgxK9|*f0 zKYAJ)XM%4T*<^xKRGs7W4lIH0Nr92>Ysv?<-lQHApO?IZGhI#^oJN{Wu=lDA2WER) zXLqa3zX|p5mUNU%FoT<%0E;lI6c|i04`-UOJ@&3VE}7sPMbX4dC(ICe60(m`zcOBR y0@7VUsys9osJ9Qr@4v<}a-3__`0000 Date: Fri, 14 Jun 2024 18:46:45 +0300 Subject: [PATCH 20/30] fix the refactor command --- Context.sublime-menu | 2 +- ask/__init__.py | 2 +- ask/commands.py | 60 ++++++++++++++++++-------- copilot/ask_view.py | 2 +- copilot/images/copilot-icon-dark.png | Bin 1626 -> 1132 bytes copilot/images/copilot-icon-light.png | Bin 1132 -> 1626 bytes event_listener.py | 2 +- 7 files changed, 46 insertions(+), 22 deletions(-) diff --git a/Context.sublime-menu b/Context.sublime-menu index 86ddec0..e6aaa24 100644 --- a/Context.sublime-menu +++ b/Context.sublime-menu @@ -5,7 +5,7 @@ "children": [ { - "caption": "Save to pieces", + "caption": "Save to Pieces", "command": "pieces_create_asset", }, { diff --git a/ask/__init__.py b/ask/__init__.py index c88b1b8..8a8b37f 100644 --- a/ask/__init__.py +++ b/ask/__init__.py @@ -1 +1 @@ -from .commands import PiecesAskQuestionCommand,ReplaceSelectionCommand \ No newline at end of file +from .commands import PiecesAskQuestionCommand,PiecesReplaceCodeSelectionCommand \ No newline at end of file diff --git a/ask/commands.py b/ask/commands.py index eb7de1b..64f9dab 100644 --- a/ask/commands.py +++ b/ask/commands.py @@ -4,7 +4,7 @@ import re from difflib import Differ import mdpopups -import time +from .diff import show_diff_popup from ..settings import PiecesSettings from .prompts import * @@ -98,16 +98,8 @@ def on_done_async(self): match = re.search(pattern, response_code, re.DOTALL) if match: self.code = match.group(1) - self.code_html = self.get_differences(self.selected_text.splitlines(),self.code.splitlines()) - link = "✅ Accept | ❌ Reject" - html = f"
{link}
{self.code_html}" - - # Calculate the length of the code_html - code_html_length = len(self.code_html) - - # Create a phantom at the end of the current selection - phantom_region = sublime.Region(self.selection.begin(), self.selection.begin() + code_html_length) - self.phantom = mdpopups.add_phantom(self.view,"code_phantom", phantom_region, html, sublime.LAYOUT_INLINE,md=False,on_navigate=self.on_nav) + show_diff_popup(self.view, self.selected_text.splitlines(), self.code.splitlines(),on_nav=self.on_nav) + self.is_done = True self.view.erase_status('Pieces Refactoring') @@ -115,11 +107,11 @@ def on_done_async(self): def on_nav(self, href): if href == "insert": # Replace the selected text with the code - self.view.run_command("replace_selection", {"code": self.code, "selection": [self.selection.a, self.selection.b]}) + self.view.run_command("pieces_replace_code_selection", {"code": self.code, "selection": [self.selection.a, self.selection.b]}) # Remove the phantom - mdpopups.erase_phantom_by_id(self.view,self.phantom) + self.view.hide_popup() elif href == "dismiss": - mdpopups.erase_phantom_by_id(self.view,self.phantom) + self.view.hide_popup() @@ -136,12 +128,44 @@ def get_differences(self,s1:list,s2:list): return final_output -class ReplaceSelectionCommand(sublime_plugin.TextCommand): +class PiecesReplaceCodeSelectionCommand(sublime_plugin.TextCommand): def run(self, edit, code, selection): # Convert the selection into a Region region = sublime.Region(selection[0], selection[1]) - # Replace the current selection with the provided code - self.view.replace(edit, region, code) - + # Retrieve the settings for tabs vs. spaces and the number of spaces per tab + settings = self.view.settings() + use_spaces = settings.get('translate_tabs_to_spaces') + tab_size = settings.get('tab_size', 4) + + # Get the current indentation level of the selected region + current_line_region = self.view.line(region.begin()) + current_line_text = self.view.substr(current_line_region) + current_indentation = self._get_indentation(current_line_text, use_spaces, tab_size) + + # Adjust the indentation of the replacement code + indented_code = self._adjust_indentation(code, current_indentation, use_spaces, tab_size) + + # Replace the current selection with the indented code + self.view.replace(edit, region, indented_code) + + def _get_indentation(self, line_text, use_spaces, tab_size): + """Calculate the indentation level of the given line.""" + indentation = 0 + for char in line_text: + if char == '\t': + indentation += tab_size + elif char == ' ': + indentation += 1 + else: + break + return indentation + + def _adjust_indentation(self, code, indentation, use_spaces, tab_size): + """Adjust the indentation of the given code.""" + lines = code.split('\n') + indent_char = ' ' * tab_size if use_spaces else '\t' + indent_string = indent_char * (indentation // tab_size) + ' ' * (indentation % tab_size) + indented_lines = [indent_string + line if line.strip() else line for line in lines] + return '\n'.join(indented_lines) diff --git a/copilot/ask_view.py b/copilot/ask_view.py index 4464c36..8a2cfbe 100644 --- a/copilot/ask_view.py +++ b/copilot/ask_view.py @@ -21,7 +21,7 @@ class CopilotViewManager: @property def gpt_view(self) -> sublime.View: - if not getattr(CopilotViewManager, "_gpt_view",None): # TODO: open the copilot in split view + if not getattr(CopilotViewManager, "_gpt_view",None): # File config and creation CopilotViewManager._gpt_view = sublime.active_window().new_file(syntax="Packages/Markdown/Markdown.sublime-syntax") CopilotViewManager.can_type = True diff --git a/copilot/images/copilot-icon-dark.png b/copilot/images/copilot-icon-dark.png index db0f3dd8943f6dfbb0e252f0564ab0ac21c271ee..e5c6dc7ab5d88874569c49eb44a3c1ddc1b2d199 100644 GIT binary patch delta 1062 zcmV+>1ljxA4D1MyReuBlNkl~ zuB&z1?&<2D9`%Rs_N}T@r%qQ_Rado04l>640^49cye~PZfPbSZu0O&DP%Y{8BK+>8 zFTpa&rWV*)C=@npVDm}*BTtw9ujHqvP;l812gl8zaZJT|^?>dZ*6ZPXr=EV4Z` z0nQjd^>XU|=~<@%e)bKh3dTZ<@z=GGkA5bdvc)ut?UGIh;VWn^Ww?Rep%L9_^oIp-SCVxhWqw%aU^LXy_C~khkw=NaQP6GB z+ptpFmVYk5l@a-GB-FbF)8QT3ngOk0gV-@<102TRPhm%lnQkn}0P!#80Jd_icLUq~xHuz{bJ> zm>n+QZaSG@JKa+{P*IDcj-h%k^)YqyaI0^I-7r8pNLgOuAK7bv;~eAimK#_f&&RW= zbbqC1TO=pZwjP>m+>9a4Vt4~)Lgxf0ufuary4KdeCMiXoZm=a3!z-o2!LZ3GEZP;m z%F}u3z)WyPooXJQKkvcn$hDKqVW#|b(*#338u1Pcn`#(t$`3sH9vzTW@fr9d6rfi8 zx;=q8evdhR_xRj+*pWW_Ii~!?v)n{Xkbk7W$R7y19Y1;+8)t%V8QEllQ&gSf^bRb6 z?n!}>?rX{iw%(*36Q7s7gfm@E8k|O&O|bW>3R delta 1560 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 diff --git a/event_listener.py b/event_listener.py index 9a82497..3c952dc 100644 --- a/event_listener.py +++ b/event_listener.py @@ -77,7 +77,7 @@ def on_init(self,views): conversation = view.settings().get("conversation_id") if conversation: on_close = lambda x:copilot.render_conversation(conversation) - view.close(on_close,5000) # Wait a sec until the conversations is loaded + view.close(on_close,10000) # Wait some sec until the conversations is loaded From 8a9b7cb99b6e69bc3439c0c2b37cf75c3d9b8074 Mon Sep 17 00:00:00 2001 From: Bishoy-at-pieces Date: Fri, 14 Jun 2024 18:47:13 +0300 Subject: [PATCH 21/30] add the diff and index.css --- ask/diff.py | 214 ++++++++++++++++++++++++++++++++++++++++++++++++++ ask/index.css | 27 +++++++ 2 files changed, 241 insertions(+) create mode 100644 ask/diff.py create mode 100644 ask/index.css diff --git a/ask/diff.py b/ask/diff.py new file mode 100644 index 0000000..3215d2c --- /dev/null +++ b/ask/diff.py @@ -0,0 +1,214 @@ +""" + Thanks to gitgutter package! + https://github.com/jisaacks/GitGutter/tree/master/modules/popup +""" +import mdpopups +from mdpopups.pygments import highlight +import sublime + +class HtmlDiffer: + """ + A class to generate HTML output highlighting the differences between two sets of lines. + """ + + def __init__(self,view, old_lines, new_lines): + """ + Initialize the HtmlDiffer with two sets of lines to compare. + + :param view: the view of the lines that you want to compare + :param old_lines: List of lines representing the old version. + :param new_lines: List of lines representing the new version. + + """ + self.old_lines = old_lines + self.new_lines = new_lines + self.view = view + + def generate_diff(self,code_wrap): + """ + Generate the HTML string with highlighted differences. + + :return: A string containing the HTML representation of the differences. + """ + return ''.join(self._highlight_diff(code_wrap)) + + def _highlight_diff(self,code_wrap): + """ + Internal generator function to yield HTML snippets with highlighted differences. + + :yield: HTML snippets as strings. + """ + div_map = self.diff_lines(self.old_lines, self.new_lines) + yield '
'
+
+		text_chunk = ""
+		old_tag = div_map[0][1]
+
+		for line, tag in div_map:
+			if tag != old_tag:
+				yield self._highlight_chunk(tag,text_chunk,code_wrap)
+				text_chunk = ""
+				old_tag = tag
+			text_chunk += line + "\n"
+
+		if text_chunk:
+			yield self._highlight_chunk(old_tag,text_chunk,code_wrap)
+		
+		yield '
' + + + def _highlight_chunk(self,_class,chunk,code_wrap): + highlighted_line = mdpopups.syntax_highlight(self.view, chunk, language=mdpopups.get_language_from_view(self.view) or '', allow_code_wrap=code_wrap) + highlighted_line = highlighted_line[28:-13] + return f'{highlighted_line}' + + + def diff_lines(self, old_lines, new_lines): + """ + Create a diff map between two sets of lines. + + :param old_lines: List of lines representing the old version. + :param new_lines: List of lines representing the new version. + :return: A list of tuples representing the diff map. + """ + # Create a 2D array to store the lengths of longest common subsequence + lcs = [[0] * (len(new_lines) + 1) for _ in range(len(old_lines) + 1)] + + # Fill the lcs array + for i in range(1, len(old_lines) + 1): + for j in range(1, len(new_lines) + 1): + if old_lines[i - 1] == new_lines[j - 1]: + lcs[i][j] = lcs[i - 1][j - 1] + 1 + else: + lcs[i][j] = max(lcs[i - 1][j], lcs[i][j - 1]) + + # Backtrack to find the diff + i, j = len(old_lines), len(new_lines) + div_map = [] + + while i > 0 and j > 0: + if old_lines[i - 1] == new_lines[j - 1]: + div_map.append((old_lines[i - 1], "unchanged")) + i -= 1 + j -= 1 + elif lcs[i - 1][j] >= lcs[i][j - 1]: + div_map.append((old_lines[i - 1], "removed")) + i -= 1 + else: + div_map.append((new_lines[j - 1], "added")) + j -= 1 + + # Add remaining lines + while i > 0: + div_map.append((old_lines[i - 1], "removed")) + i -= 1 + while j > 0: + div_map.append((new_lines[j - 1], "added")) + j -= 1 + + # Reverse to get the correct order + div_map.reverse() + return div_map + + + + +def show_diff_popup(view, old_lines, new_lines, on_nav,**kwargs): + """Show the diff popup. + + Arguments: + view (sublime.View): + The view object where the popup will be displayed. + old_lines (list): + The list of lines representing the old version. + new_lines (list): + The list of lines representing the new version. + on_nav (callback): + the callback that will be runned when the buttons is clicked + kwargs (dict): + Additional arguments for customization. + """ + point = kwargs.get('point', view.sel()[0].end() if view.sel() else None) + if point is None: + return + + line = view.rowcol(point)[0] + 1 + + buttons = "
✅ Accept | ❌ Reject" + location = _visible_text_point(view, line - 1, 0) + code_wrap = view.settings().get('word_wrap', 'auto') == 'auto' and view.match_selector(location, 'source') + + + differ = HtmlDiffer(view,old_lines, new_lines).generate_diff(code_wrap=code_wrap) + + content = ( + buttons + + differ + ) + + popup_kwargs = { + 'view': view, + 'content': content, + 'md': False, + 'css': _load_popup_css() + } + + popup_width = int(view.viewport_extent()[0]) + if code_wrap: + line_length = view.settings().get('wrap_width', 0) + if line_length > 0: + popup_width = (line_length + 5) * view.em_width() + mdpopups.show_popup(location=location, max_width=popup_width, flags=kwargs.get('flags', 0), on_navigate=on_nav, **popup_kwargs) + + + + +def _load_popup_css(): + """Load and join popup stylesheets.""" + css_lines = [] + for path in ('Packages/Pieces', 'Packages/User'): + try: + css_path = path + '/ask/index.css' + css_lines.append(sublime.load_resource(css_path)) + except IOError: + pass + return ''.join(css_lines) + + +def _get_min_indent(lines, tab_width=4): + """Find the minimum count of indenting whitespaces in lines. + + Arguments: + lines (tuple): The content to search the minimum indention for. + tab_width (int): The number of spaces expand tabs before searching for indention by. + """ + min_indent = 2**32 + for line in lines: + i = 0 + for c in line: + if c == ' ': + i += 1 + elif c == '\t': + i += tab_width - (i % tab_width) + else: + break + if min_indent > i: + min_indent = i + if not min_indent: + break + return min_indent + + +def _visible_text_point(view, row, col): + """Return the text_point of row,col clipped to the visible viewport. + + Arguments: + view (sublime.View): the view to return the text_point for + row (int): the row to use for text_point calculation + col (int): the column relative to the first visible column of the viewport which is defined by the horizontal scroll position. + Returns: + int: The text_point of row & col within the viewport. + """ + viewport = view.visible_region() + _, vp_col = view.rowcol(viewport.begin()) + return view.text_point(row, vp_col + col) diff --git a/ask/index.css b/ask/index.css new file mode 100644 index 0000000..89b9b37 --- /dev/null +++ b/ask/index.css @@ -0,0 +1,27 @@ +/* Many thanks to gitgutter! */ +html.dark { + --popup-background: color(var(--background) blend(white 95%)); + --pieces-toolbar-bg: color(var(--popup-background) blend(white 95%)); +} + +html.light { + --popup-background: color(var(--background) blend(black 95%)); + --pieces-toolbar-bg: color(var(--popup-background) blend(black 91%)); + --pieces-greenish: color(var(--greenish) blend(black 40%)); + --pieces-redish: color(var(--redish) blend(black 40%)); +} + +.toolbar { + background-color: var(--pieces-toolbar-bg); + color: color(var(--foreground) blend(var(--pieces-toolbar-bg) 30%)); + margin: 0; + padding: 0.0rem 0.4rem 0.2rem 0.4em; +} +.removed { + background-color: var(--pieces-redish); +} +.added{ + background-color: var(--pieces-greenish); +} + + From a097eafe3b741b49a736697eddf7245f6059c7dd Mon Sep 17 00:00:00 2001 From: Bishoy-at-pieces Date: Mon, 17 Jun 2024 13:48:55 +0300 Subject: [PATCH 22/30] upgrade the refactor command to use the pipelines #29 --- Context.sublime-menu | 15 +-- ask/commands.py | 224 ++++++++++++++++++++++++------------------- ask/diff.py | 156 +++++++++++++++--------------- ask/index.css | 6 +- ask/prompts.py | 23 ----- 5 files changed, 207 insertions(+), 217 deletions(-) delete mode 100644 ask/prompts.py diff --git a/Context.sublime-menu b/Context.sublime-menu index e6aaa24..0cab656 100644 --- a/Context.sublime-menu +++ b/Context.sublime-menu @@ -15,22 +15,17 @@ { "caption": "Fix a bug", "command": "pieces_ask_question", - "args":{"question":"bug"} + "args":{"task":"fix"} }, { - "caption": "Refactor", + "caption": "Modify", "command": "pieces_ask_question", - "args":{"question":"refactor"} + "args":{"task":"modify"} }, { - "caption":"Add a doc string to this function", + "caption":"Add code comments", "command": "pieces_ask_question", - "args":{"question":"docstring"} - }, - { - "caption": "Add comments to the code", - "command": "pieces_ask_question", - "args":{"question":"comment"} + "args":{"task":"comment"} } ] } diff --git a/ask/commands.py b/ask/commands.py index 64f9dab..c3c38cd 100644 --- a/ask/commands.py +++ b/ask/commands.py @@ -1,51 +1,72 @@ import sublime import sublime_plugin -import pieces_os_client as pos_client +import textwrap +from pieces_os_client import ( + QGPTTaskPipelineForCodeFix, + QGPTTaskPipeline, + QGPTTaskPipelineForCodeCommentation, + QGPTTaskPipelineForCodeModification, + QGPTApi, + Seed, + SeededFormat, + SeededAsset, + SeededFragment, + RelevantQGPTSeed, + QGPTPromptPipeline, + RelevantQGPTSeeds, + TransferableString, + QGPTQuestionInput, + ClassificationSpecificEnum, + SeededClassification) import re -from difflib import Differ import mdpopups -from .diff import show_diff_popup +from .diff import show_diff_popup from ..settings import PiecesSettings -from .prompts import * - -prompt_map = {"bug":BUGS_PROMPT, -"refactor": CLEANER_CODE_PROMPT, -"docstring":DOC_STRING_PROMPT, -"comment":ADD_COMMENTS_PROMPT} - - +description_needed_commands = { + "modify":"Enter the instructions that should we use to modify that code", + "fix":"Enter the error message that you got" +} class PiecesAskQuestionCommand(sublime_plugin.TextCommand): def is_enabled(self): return PiecesSettings().is_loaded - def run(self,edit, question): - sublime.set_timeout_async(lambda:self.run_async(edit,question),0) + def run(self,edit, task): + # task = comment,fix,modify + self.task = task + sublime.set_timeout_async(self.run_async,0) - def run_async(self,edit,question): - self.question = prompt_map[question] - + def run_async(self): # Get the current selection self.selection = self.view.sel()[0] - self.selected_text = self.view.substr(self.selection) - + self.selected_text = textwrap.dedent(self.view.substr(self.selection)) + print(self.selected_text) + + # Getting the langauge try: - self.langauge = self.view.file_name().split(".")[-1] + ext = self.view.file_name().split(".")[-1] + + if ext in ClassificationSpecificEnum: + self.classification = SeededClassification(specific = ext) + else: + raise AttributeError except: - self.langauge = "txt" + self.classification = None if not self.selected_text: sublime.error_message("Please select a text to ask about!") return - if self.question in description_needed_commands: - sublime.active_window().show_input_panel("Enter a description:", "", self.on_done, None, None) + description_placeholder = description_needed_commands.get(self.task) + + if description_placeholder: + sublime.active_window().show_input_panel(description_placeholder, "", self.on_done, None, None) else: self.on_done_async() @@ -56,39 +77,50 @@ def on_done(self,description): sublime.set_timeout_async(self.on_done_async,0) def on_done_async(self): - self.view.set_status('Pieces Refactoring', 'Copilot is thinking...') - - query = self.question.format(description=self.description,code=self.selected_text) if self.description else self.question.format(code=self.selected_text) + if self.task == "fix": + pipeline = QGPTTaskPipeline(code_fix=QGPTTaskPipelineForCodeFix(error=self.description)) + elif self.task == "modify": + pipeline = QGPTTaskPipeline(code_modification=QGPTTaskPipelineForCodeModification(instruction=self.description)) + elif self.task == "comment": + pipeline = QGPTTaskPipeline(code_commentation=QGPTTaskPipelineForCodeCommentation()) + self.view.set_status('Pieces Refactoring', 'Copilot is thinking...') - res = pos_client.QGPTApi(PiecesSettings.api_client).question( - pos_client.QGPTQuestionInput( - query = query, - model = PiecesSettings.model_id, - relevant = pos_client.RelevantQGPTSeeds( - iterable = [ - # TODO: Use the pipeline prompts - # pos_client.RelevantQGPTSeed( - # seed = pos_client.Seed( - # type="SEEDED_ASSET", - # asset=pos_client.SeededAsset( - # application=PiecesSettings.application, - # format=pos_client.SeededFormat( - # fragment = pos_client.SeededFragment( - # string = pos_client.TransferableString(raw = selected_text) - # ), - # ), - # ), - # ), - # ) - ] - ) + gpt_input = QGPTQuestionInput( + query = " ", + model = PiecesSettings.model_id, + application = PiecesSettings.application.id, + pipeline = QGPTPromptPipeline( + task = pipeline + ), + relevant = RelevantQGPTSeeds( + iterable = [ + RelevantQGPTSeed( + seed = Seed( + type="SEEDED_ASSET", + asset=SeededAsset( + application=PiecesSettings.application, + format=SeededFormat( + fragment = SeededFragment( + string = TransferableString(raw = self.selected_text) + ), + classification = self.classification + ), + ), + ), + ) + ] ) ) - + try: + res = QGPTApi(PiecesSettings.api_client).question(gpt_input) + except: + self.view.set_status('Pieces Refactoring', 'Copilot error in getting the responses') + sublime.set_timeout(lambda:self.view.erase_status("Pieces Refactoring"),5000) self.view.set_status('Pieces Refactoring', 'Copilot analyzing...') self.window = self.view.window() + response_code = res.answers.iterable[0].text # Regular expression pattern for code block @@ -101,71 +133,61 @@ def on_done_async(self): show_diff_popup(self.view, self.selected_text.splitlines(), self.code.splitlines(),on_nav=self.on_nav) self.is_done = True - self.view.erase_status('Pieces Refactoring') + else: + mdpopups.show_popup(self.view,response_code,md=True) # No code found + self.view.erase_status('Pieces Refactoring') def on_nav(self, href): if href == "insert": # Replace the selected text with the code self.view.run_command("pieces_replace_code_selection", {"code": self.code, "selection": [self.selection.a, self.selection.b]}) - # Remove the phantom + # Remove the popup self.view.hide_popup() elif href == "dismiss": self.view.hide_popup() - - - - def get_differences(self,s1:list,s2:list): - - # Compare the snippets - diffs = Differ().compare(s1, s2) - - final_output = "\n".join(diffs) - - final_output = mdpopups.md2html(self.view,f"```{self.langauge}\n{final_output}\n```") - return final_output class PiecesReplaceCodeSelectionCommand(sublime_plugin.TextCommand): - def run(self, edit, code, selection): - # Convert the selection into a Region - region = sublime.Region(selection[0], selection[1]) - - # Retrieve the settings for tabs vs. spaces and the number of spaces per tab - settings = self.view.settings() - use_spaces = settings.get('translate_tabs_to_spaces') - tab_size = settings.get('tab_size', 4) - - # Get the current indentation level of the selected region - current_line_region = self.view.line(region.begin()) - current_line_text = self.view.substr(current_line_region) - current_indentation = self._get_indentation(current_line_text, use_spaces, tab_size) - - # Adjust the indentation of the replacement code - indented_code = self._adjust_indentation(code, current_indentation, use_spaces, tab_size) - - # Replace the current selection with the indented code - self.view.replace(edit, region, indented_code) - - def _get_indentation(self, line_text, use_spaces, tab_size): - """Calculate the indentation level of the given line.""" - indentation = 0 - for char in line_text: - if char == '\t': - indentation += tab_size - elif char == ' ': - indentation += 1 - else: - break - return indentation - - def _adjust_indentation(self, code, indentation, use_spaces, tab_size): - """Adjust the indentation of the given code.""" - lines = code.split('\n') - indent_char = ' ' * tab_size if use_spaces else '\t' - indent_string = indent_char * (indentation // tab_size) + ' ' * (indentation % tab_size) - indented_lines = [indent_string + line if line.strip() else line for line in lines] - return '\n'.join(indented_lines) + def run(self, edit, code, selection): + # Convert the selection into a Region + region = sublime.Region(selection[0], selection[1]) + + # Retrieve the settings for tabs vs. spaces and the number of spaces per tab + settings = self.view.settings() + use_spaces = settings.get('translate_tabs_to_spaces') + tab_size = settings.get('tab_size', 4) + + # Get the current indentation level of the selected region + current_line_region = self.view.line(region.begin()) + current_line_text = self.view.substr(current_line_region) + current_indentation = self._get_indentation(current_line_text, tab_size) + + # Adjust the indentation of the replacement code + indented_code = self._adjust_indentation(code, current_indentation, use_spaces, tab_size) + + # Replace the current selection with the indented code + self.view.replace(edit, region, indented_code) + + def _get_indentation(self, line_text, tab_size): + """Calculate the indentation level of the given line.""" + indentation = 0 + for char in line_text: + if char == '\t': + indentation += tab_size + elif char == ' ': + indentation += 1 + else: + break + return indentation + + def _adjust_indentation(self, code, indentation, use_spaces, tab_size): + """Adjust the indentation of the given code.""" + lines = code.split('\n') + indent_char = ' ' * tab_size if use_spaces else '\t' + indent_string = indent_char * (indentation // tab_size) + ' ' * (indentation % tab_size) + indented_lines = [indent_string + line if line.strip() else line for line in lines] + return '\n'.join(indented_lines) diff --git a/ask/diff.py b/ask/diff.py index 3215d2c..8317233 100644 --- a/ask/diff.py +++ b/ask/diff.py @@ -3,7 +3,6 @@ https://github.com/jisaacks/GitGutter/tree/master/modules/popup """ import mdpopups -from mdpopups.pygments import highlight import sublime class HtmlDiffer: @@ -59,58 +58,79 @@ def _highlight_diff(self,code_wrap): def _highlight_chunk(self,_class,chunk,code_wrap): highlighted_line = mdpopups.syntax_highlight(self.view, chunk, language=mdpopups.get_language_from_view(self.view) or '', allow_code_wrap=code_wrap) - highlighted_line = highlighted_line[28:-13] + highlighted_line = highlighted_line[28:-13] # Remove the pre,div begining classes return f'{highlighted_line}' def diff_lines(self, old_lines, new_lines): """ - Create a diff map between two sets of lines. + Create a diff map between two sets of lines using Myers' diff algorithm. :param old_lines: List of lines representing the old version. :param new_lines: List of lines representing the new version. :return: A list of tuples representing the diff map. """ - # Create a 2D array to store the lengths of longest common subsequence - lcs = [[0] * (len(new_lines) + 1) for _ in range(len(old_lines) + 1)] - - # Fill the lcs array - for i in range(1, len(old_lines) + 1): - for j in range(1, len(new_lines) + 1): - if old_lines[i - 1] == new_lines[j - 1]: - lcs[i][j] = lcs[i - 1][j - 1] + 1 + trace = self.build_trace(old_lines, new_lines) + return self.build_diff(trace, old_lines, new_lines) + + @staticmethod + def build_trace(old_lines, new_lines): + n, m = len(old_lines), len(new_lines) + max_d = n + m + v = [0] * (2 * max_d + 1) + trace = [] + + for d in range(max_d + 1): + trace.append(v[:]) + for k in range(-d, d + 1, 2): + if k == -d or (k != d and v[k - 1] < v[k + 1]): + x = v[k + 1] else: - lcs[i][j] = max(lcs[i - 1][j], lcs[i][j - 1]) - - # Backtrack to find the diff - i, j = len(old_lines), len(new_lines) - div_map = [] - - while i > 0 and j > 0: - if old_lines[i - 1] == new_lines[j - 1]: - div_map.append((old_lines[i - 1], "unchanged")) - i -= 1 - j -= 1 - elif lcs[i - 1][j] >= lcs[i][j - 1]: - div_map.append((old_lines[i - 1], "removed")) - i -= 1 + x = v[k - 1] + 1 + y = x - k + while x < n and y < m and old_lines[x] == new_lines[y]: + x += 1 + y += 1 + v[k] = x + if x >= n and y >= m: + trace.append(v[:]) + return trace + + @staticmethod + def build_diff(trace, old_lines, new_lines): + n, m = len(old_lines), len(new_lines) + x, y = n, m + diff_map = [] + + for d in range(len(trace) - 1, -1, -1): + v = trace[d] + k = x - y + if k == -d or (k != d and v[k - 1] < v[k + 1]): + prev_k = k + 1 else: - div_map.append((new_lines[j - 1], "added")) - j -= 1 - - # Add remaining lines - while i > 0: - div_map.append((old_lines[i - 1], "removed")) - i -= 1 - while j > 0: - div_map.append((new_lines[j - 1], "added")) - j -= 1 - - # Reverse to get the correct order - div_map.reverse() - return div_map - - + prev_k = k - 1 + prev_x = v[prev_k] + prev_y = prev_x - prev_k + while x > prev_x and y > prev_y: + diff_map.append((old_lines[x - 1], "unchanged")) + x -= 1 + y -= 1 + if x > prev_x: + diff_map.append((old_lines[x - 1], "removed")) + x -= 1 + elif y > prev_y: + diff_map.append((new_lines[y - 1], "added")) + y -= 1 + + while x > 0: + diff_map.append((old_lines[x - 1], "removed")) + x -= 1 + while y > 0: + diff_map.append((new_lines[y - 1], "added")) + y -= 1 + + diff_map.reverse() + return diff_map def show_diff_popup(view, old_lines, new_lines, on_nav,**kwargs): @@ -146,57 +166,31 @@ def show_diff_popup(view, old_lines, new_lines, on_nav,**kwargs): differ ) - popup_kwargs = { - 'view': view, - 'content': content, - 'md': False, - 'css': _load_popup_css() - } - popup_width = int(view.viewport_extent()[0]) if code_wrap: line_length = view.settings().get('wrap_width', 0) if line_length > 0: popup_width = (line_length + 5) * view.em_width() - mdpopups.show_popup(location=location, max_width=popup_width, flags=kwargs.get('flags', 0), on_navigate=on_nav, **popup_kwargs) + popup_kwargs = { + 'view': view, + 'content': content, + 'md': False, + 'css': _load_popup_css(), + "location": location, + "max_width": popup_width, + "flags":kwargs.get('flags', 0), + "on_navigate":on_nav + } + + mdpopups.show_popup(**popup_kwargs,on_hide=lambda:mdpopups.show_popup(**popup_kwargs)) def _load_popup_css(): """Load and join popup stylesheets.""" - css_lines = [] - for path in ('Packages/Pieces', 'Packages/User'): - try: - css_path = path + '/ask/index.css' - css_lines.append(sublime.load_resource(css_path)) - except IOError: - pass - return ''.join(css_lines) - - -def _get_min_indent(lines, tab_width=4): - """Find the minimum count of indenting whitespaces in lines. - - Arguments: - lines (tuple): The content to search the minimum indention for. - tab_width (int): The number of spaces expand tabs before searching for indention by. - """ - min_indent = 2**32 - for line in lines: - i = 0 - for c in line: - if c == ' ': - i += 1 - elif c == '\t': - i += tab_width - (i % tab_width) - else: - break - if min_indent > i: - min_indent = i - if not min_indent: - break - return min_indent + css_path = 'Packages/Pieces/ask/index.css' + return sublime.load_resource(css_path) def _visible_text_point(view, row, col): diff --git a/ask/index.css b/ask/index.css index 89b9b37..9a004f0 100644 --- a/ask/index.css +++ b/ask/index.css @@ -2,13 +2,15 @@ html.dark { --popup-background: color(var(--background) blend(white 95%)); --pieces-toolbar-bg: color(var(--popup-background) blend(white 95%)); + --pieces-greenish: color(var(--greenish) blend(black 40%)); + --pieces-redish: color(var(--redish) blend(black 40%)); } html.light { --popup-background: color(var(--background) blend(black 95%)); --pieces-toolbar-bg: color(var(--popup-background) blend(black 91%)); - --pieces-greenish: color(var(--greenish) blend(black 40%)); - --pieces-redish: color(var(--redish) blend(black 40%)); + --pieces-greenish: color(var(--greenish) blend(white 40%)); + --pieces-redish: color(var(--redish) blend(white 40%)); } .toolbar { diff --git a/ask/prompts.py b/ask/prompts.py deleted file mode 100644 index 36ede84..0000000 --- a/ask/prompts.py +++ /dev/null @@ -1,23 +0,0 @@ -BUGS_PROMPT = """Act as a programmer to fix this bug -There is a bug in this codebase fix it provided to you -Here is a quick description: {description} -Here is the code: {code} -""" - -CLEANER_CODE_PROMPT = """Act as a programmer to clean that code and -write it in a better way and more pythonic one -Here is the code: {code}""" - -DOC_STRING_PROMPT = """ -Generate a doc string to that code -Here is the code: {code} -""" - - -ADD_COMMENTS_PROMPT=""" -Add some comments to that code to be much more easier to read -Here is the code: {code} -""" - - -description_needed_commands = [BUGS_PROMPT] \ No newline at end of file From aa7882efc1d82df2ada29e0927bda3ecf862bf83 Mon Sep 17 00:00:00 2001 From: Bishoy-at-pieces Date: Mon, 17 Jun 2024 14:29:09 +0300 Subject: [PATCH 23/30] better error handling --- ask/commands.py | 8 +++++--- ask/diff.py | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/ask/commands.py b/ask/commands.py index c3c38cd..8a201eb 100644 --- a/ask/commands.py +++ b/ask/commands.py @@ -39,14 +39,15 @@ def run(self,edit, task): self.task = task sublime.set_timeout_async(self.run_async,0) - - + + def run_async(self): # Get the current selection self.selection = self.view.sel()[0] + # Modify the selection to whole line not part of it + self.selection = sublime.Region(self.view.line(self.selection.begin()).begin(),self.selection.end()) self.selected_text = textwrap.dedent(self.view.substr(self.selection)) - print(self.selected_text) # Getting the langauge try: @@ -118,6 +119,7 @@ def on_done_async(self): except: self.view.set_status('Pieces Refactoring', 'Copilot error in getting the responses') sublime.set_timeout(lambda:self.view.erase_status("Pieces Refactoring"),5000) + return self.view.set_status('Pieces Refactoring', 'Copilot analyzing...') self.window = self.view.window() diff --git a/ask/diff.py b/ask/diff.py index 8317233..0c7a269 100644 --- a/ask/diff.py +++ b/ask/diff.py @@ -182,7 +182,8 @@ def show_diff_popup(view, old_lines, new_lines, on_nav,**kwargs): "flags":kwargs.get('flags', 0), "on_navigate":on_nav } - + + view.hide_popup() # Remove any other popup in the view mdpopups.show_popup(**popup_kwargs,on_hide=lambda:mdpopups.show_popup(**popup_kwargs)) From 5be531063e4f632e1b1f9a5e6d9c789c946dfacf Mon Sep 17 00:00:00 2001 From: Bishoy-at-pieces Date: Thu, 20 Jun 2024 16:29:37 +0300 Subject: [PATCH 24/30] add failed status --- copilot/ask_view.py | 31 +++++++++++++++++++++++++++++-- copilot/images/warning.png | Bin 0 -> 8614 bytes event_listener.py | 2 +- 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 copilot/images/warning.png diff --git a/copilot/ask_view.py b/copilot/ask_view.py index 8a2cfbe..354a2a8 100644 --- a/copilot/ask_view.py +++ b/copilot/ask_view.py @@ -37,6 +37,11 @@ def gpt_view(self) -> sublime.View: self.phantom_details_dict = {} # id: {"code":code,"region":region} + + # Failed regions + self.failed_regions = [] + self.failed_phantom = sublime.PhantomSet(CopilotViewManager._gpt_view, "Pieces_Failed_Phantoms") + # Others self.copilot_regions = [] self.show_cursor @@ -114,10 +119,32 @@ def on_message_callback(self,message: QGPTStreamOutput): if message.status == "COMPLETED": self.new_line() self.end_response = self.gpt_view.size() # Update the size - self.show_cursor - CopilotViewManager.can_type = True + self.reset_view() self.conversation_id = message.conversation self.add_code_phantoms() # Generate the code phantoms + elif message.status == "FAILED": + self.failed_regions.append(self.copilot_regions.pop()) + self.show_failed() + self.reset_view() + + + def show_failed(self): + self.gpt_view.add_regions( + "pieces", + self.failed_regions, + scope="region.yellowish", + icon=f"Packages/Pieces/copilot/images/warning.png", + flags=sublime.HIDDEN + ) + self.failed_phantom.update( + [sublime.Phantom(region,"Something went wrong",sublime.LAYOUT_BLOCK) for region in self.failed_regions] # TODO: Add retry + ) + + + def reset_view(self): + self.show_cursor + CopilotViewManager.can_type = True + @property def conversation_id(self): return self.gpt_view.settings().get("conversation_id") diff --git a/copilot/images/warning.png b/copilot/images/warning.png new file mode 100644 index 0000000000000000000000000000000000000000..e723999b3bc2fad56db659f37cd7e9601c38922f GIT binary patch literal 8614 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 Date: Fri, 21 Jun 2024 20:26:21 +0300 Subject: [PATCH 25/30] change the basewebsocket and the streamedidentifiers to abstract classes --- base_websocket.py | 18 +++++++++++++----- copilot/ask_view.py | 19 ++++--------------- streamed_identifiers.py | 7 ++++--- 3 files changed, 21 insertions(+), 23 deletions(-) diff --git a/base_websocket.py b/base_websocket.py index c5daa61..25383c7 100644 --- a/base_websocket.py +++ b/base_websocket.py @@ -2,11 +2,12 @@ import sublime_plugin import websocket import threading +from abc import ABC, abstractmethod -class BaseWebsocket: +class BaseWebsocket(ABC): instances = [] - def __init__(self,on_message_callback=None): + def __init__(self, on_message_callback=None): self.ws = None self.thread = None self.running = False @@ -14,8 +15,13 @@ def __init__(self,on_message_callback=None): BaseWebsocket.instances.append(self) + @abstractmethod + def url(self): + pass + - def on_message(self, ws,message): + @abstractmethod + def on_message(self, ws, message): pass def on_error(self, ws, error): @@ -24,7 +30,7 @@ def on_error(self, ws, error): def on_close(self, ws, close_status_code, close_msg): pass - def on_open(self,ws): + def on_open(self, ws): self.running = True def run(self): @@ -47,6 +53,7 @@ def close(self): self.ws.close() self.thread.join() self.running = False + @classmethod def close_all(cls): for instance in cls.instances: @@ -64,4 +71,5 @@ def reconnect(self): self.start() def __str__(self): - return getattr(self,"url",self.instances) \ No newline at end of file + return getattr(self, "url", self.instances) + diff --git a/copilot/ask_view.py b/copilot/ask_view.py index 354a2a8..9e1abbb 100644 --- a/copilot/ask_view.py +++ b/copilot/ask_view.py @@ -6,7 +6,6 @@ from sublime import Region import re - PHANTOM_A_TAG_STYLE = "padding: 4px;background-color: var(--accent); border-radius: 6px;color: var(--foreground);text-decoration: None;text-align: center" PHANTOM_CONTENT = f""" @@ -46,7 +45,7 @@ def gpt_view(self) -> sublime.View: self.copilot_regions = [] self.show_cursor self.update_status_bar() - self.render_copilot_image_phantom(CopilotViewManager._gpt_view) + # self.render_copilot_image_phantom(CopilotViewManager._gpt_view) # Create a new group (split view) @@ -218,7 +217,7 @@ def on_nav(self,href:str): if command == "save": self.gpt_view.run_command("pieces_create_asset",{"data":code}) self.update_phantom_set(region,id,"Saving") - sublime.set_timeout_async(lambda:self.update_phantom_set(region,id,"Saved"),5000) + sublime.set_timeout_async(lambda:self.update_phantom_set(region,id,"Save"),5000) # TODO: Change it to a view button elif command == "copy": sublime.set_clipboard(code) @@ -242,17 +241,7 @@ def update_phantom_set(self,region,id,save="Save",copy="Copy",reset = False): phantoms = [phantom] self.phantom_set.update(phantoms) - @staticmethod - def render_copilot_image_phantom(view:sublime.View): - pass - # ui = sublime.ui_info()["theme"]["style"] - # view.run_command("append",{"characters":"\n"}) - # view.add_phantom( - # key="Pieces_image", - # region = sublime.Region(0,60), - # layout = sublime.LAYOUT_INLINE, - # content =f"" - # ) + def render_conversation(self,conversation_id): @@ -281,7 +270,7 @@ def render_conversation(self,conversation_id): self.select_end if val == -1: # message is deleted continue - message = message_api.message_specific_message_snapshot(message=key) + message = message_api.message_specific_message_snapshot(message=key,transferables=True) if message.role == "USER": self.show_cursor diff --git a/streamed_identifiers.py b/streamed_identifiers.py index c6e0238..f40a8ea 100644 --- a/streamed_identifiers.py +++ b/streamed_identifiers.py @@ -26,9 +26,10 @@ class AssetSnapshot(StreamedIdentifiersCache,api_call=AssetApi(PiecesSettings.ap from typing import Dict, Union, Callable from pieces_os_client import Conversation, StreamedIdentifiers, Asset import sublime +from abc import ABC,abstractmethod -class StreamedIdentifiersCache: +class StreamedIdentifiersCache(ABC): """ This class is made for caching Streamed Identifiers. Please use this class only as a parent class. @@ -44,8 +45,8 @@ def __init_subclass__(cls, api_call: Callable[[str], Union[Asset, Conversation]] cls.first_shot = True # First time to open the websocket or not - @classmethod - def sort_first_shot(cls): + @abstractmethod + def sort_first_shot(): """ Sorting algrothim in the first shot """ From bdfb7eac274360a6fed10f02f12463ce3228aeca Mon Sep 17 00:00:00 2001 From: Bishoy-at-pieces Date: Sat, 22 Jun 2024 18:20:55 +0300 Subject: [PATCH 26/30] change imports --- copilot/ask_command.py | 2 +- copilot/ask_view.py | 2 +- copilot/ask_websocket.py | 2 +- copilot/conversation_websocket.py | 2 +- copilot/conversations.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/copilot/ask_command.py b/copilot/ask_command.py index 234cb95..18890d0 100644 --- a/copilot/ask_command.py +++ b/copilot/ask_command.py @@ -1,4 +1,4 @@ -from pieces_os_client import AnnotationApi +from .._pieces_lib.pieces_os_client import AnnotationApi import sublime_plugin import sublime from .ask_view import CopilotViewManager diff --git a/copilot/ask_view.py b/copilot/ask_view.py index 9e1abbb..7f5b294 100644 --- a/copilot/ask_view.py +++ b/copilot/ask_view.py @@ -1,7 +1,7 @@ import sublime from .ask_websocket import AskStreamWS from .conversations import ConversationsSnapshot -from pieces_os_client import ConversationMessageApi, QGPTQuestionInput, QGPTStreamInput, RelevantQGPTSeeds,QGPTStreamOutput +from .._pieces_lib.pieces_os_client import ConversationMessageApi, QGPTQuestionInput, QGPTStreamInput, RelevantQGPTSeeds,QGPTStreamOutput from ..settings import PiecesSettings from sublime import Region import re diff --git a/copilot/ask_websocket.py b/copilot/ask_websocket.py index 669da48..49b0ae0 100644 --- a/copilot/ask_websocket.py +++ b/copilot/ask_websocket.py @@ -1,4 +1,4 @@ -from pieces_os_client import QGPTStreamOutput,QGPTStreamInput +from .._pieces_lib.pieces_os_client import QGPTStreamOutput,QGPTStreamInput from websocket import WebSocketConnectionClosedException from ..settings import PiecesSettings diff --git a/copilot/conversation_websocket.py b/copilot/conversation_websocket.py index 02152d2..d50bb4a 100644 --- a/copilot/conversation_websocket.py +++ b/copilot/conversation_websocket.py @@ -1,4 +1,4 @@ -from pieces_os_client import StreamedIdentifiers +from .._pieces_lib.pieces_os_client import StreamedIdentifiers from ..settings import PiecesSettings from ..base_websocket import BaseWebsocket diff --git a/copilot/conversations.py b/copilot/conversations.py index 4e0b360..42dfd89 100644 --- a/copilot/conversations.py +++ b/copilot/conversations.py @@ -1,6 +1,6 @@ from ..streamed_identifiers import StreamedIdentifiersCache from ..settings import PiecesSettings -from pieces_os_client import ConversationApi +from .._pieces_lib.pieces_os_client import ConversationApi class ConversationsSnapshot(StreamedIdentifiersCache, api_call=ConversationApi(PiecesSettings.api_client).conversation_get_specific_conversation): From 2481dc976020fa737c06ca20380ca1913e976f9c Mon Sep 17 00:00:00 2001 From: Bishoy-at-pieces Date: Sat, 22 Jun 2024 18:24:01 +0300 Subject: [PATCH 27/30] fix the assets snapshot --- assets/list_assets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/list_assets.py b/assets/list_assets.py index e1b435a..4fec874 100644 --- a/assets/list_assets.py +++ b/assets/list_assets.py @@ -22,7 +22,7 @@ def run_async(self,pieces_asset_id): try: api_response = api_instance.asset_specific_asset_export(pieces_asset_id, "MD") except: - AssetSnapshot.assets_snapshot.pop(pieces_asset_id) + AssetSnapshot.identifiers_snapshot.pop(pieces_asset_id) return sublime.error_message("Asset Not Found") markdown_text = api_response.raw.string.raw From 361d6700e8f1b3faadae08833793b527202f0483 Mon Sep 17 00:00:00 2001 From: Bishoy-at-pieces Date: Sat, 22 Jun 2024 18:27:58 +0300 Subject: [PATCH 28/30] remove extra R --- copilot/ask_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/copilot/ask_command.py b/copilot/ask_command.py index 18890d0..b5c941a 100644 --- a/copilot/ask_command.py +++ b/copilot/ask_command.py @@ -13,7 +13,7 @@ def run(self,pieces_choose_type,pieces_query=None,pieces_conversation_id=None): copilot.ask_websocket.start() copilot.render_conversation(pieces_conversation_id) if pieces_query: - copilot.gpt_view.run_command("append",{"charaters":pieces_queryR}) + copilot.gpt_view.run_command("append",{"charaters":pieces_query}) copilot.gpt_view.run_command("pieces_enter_response") return From a30fb82133b47c1adee05da38c2fb50906441932 Mon Sep 17 00:00:00 2001 From: Bishoy-at-pieces Date: Mon, 24 Jun 2024 12:28:57 +0300 Subject: [PATCH 29/30] edit the keybindings --- keybindings/Default.sublime-keymap | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/keybindings/Default.sublime-keymap b/keybindings/Default.sublime-keymap index fc9c6df..ffd2e84 100644 --- a/keybindings/Default.sublime-keymap +++ b/keybindings/Default.sublime-keymap @@ -15,11 +15,12 @@ { "keys": ["tab"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, { "keys": ["space"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, { "keys": ["backspace"], "command": "noop", "context": [{"key": "pieces_copilot_remove"}]}, + { "keys": ["delete"], "command": "noop", "context": [{"key": "pieces_copilot_remove"}]}, { "keys": ["enter"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, { "keys": ["shift+insert"], "command": "noop","context": [{"key": "pieces_copilot_add"}]}, { "keys": ["shift+tab"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, { "keys": ["shift+backspace"], "command": "noop","context": [{"key": "pieces_copilot_remove"}]}, - { "keys": ["ctrl+backspace"], "command": "noop","context": [{"key": "pieces_copilot_remove"}]}, + { "keys": ["primary+backspace"], "command": "noop","context": [{"key": "pieces_copilot_remove"}]}, // Basic keys { "keys": ["A"], "command": "noop", "context": [{"key": "pieces_copilot_add"}]}, From 43a0505518562c4c05a771c1bfd3d3b70115c701 Mon Sep 17 00:00:00 2001 From: Bishoy-at-pieces Date: Tue, 25 Jun 2024 11:39:23 +0300 Subject: [PATCH 30/30] change assets snapshot -> identifiers_snapshot --- Pieces.sublime-commands | 2 +- tests/test_create_and_delete_asset.py | 6 +++--- tests/test_list_assets.py | 4 ++-- tests/test_markdown_assets.py | 4 ++-- tests/test_search.py | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Pieces.sublime-commands b/Pieces.sublime-commands index a3d1e55..9961b2f 100644 --- a/Pieces.sublime-commands +++ b/Pieces.sublime-commands @@ -45,7 +45,7 @@ "caption": "Pieces: About", "command": "pieces_about" }, - { + { "caption": "Pieces: Open Pieces Settings", "command": "edit_settings", "args": diff --git a/tests/test_create_and_delete_asset.py b/tests/test_create_and_delete_asset.py index cf69d72..8ef0a4a 100644 --- a/tests/test_create_and_delete_asset.py +++ b/tests/test_create_and_delete_asset.py @@ -24,8 +24,8 @@ def test_create_command(self): self.view.run_command("pieces_create_asset",args={"data":self.text_asset}) yield 1000 - TestCreateAndDeleteCommand.asset_id = list(AssetSnapshot.assets_snapshot.keys())[0] - raw = AssetSnapshot.assets_snapshot[TestCreateAndDeleteCommand.asset_id].original.reference.fragment.string.raw + TestCreateAndDeleteCommand.asset_id = list(AssetSnapshot.identifiers_snapshot.keys())[0] + raw = AssetSnapshot.identifiers_snapshot[TestCreateAndDeleteCommand.asset_id].original.reference.fragment.string.raw self.assertEqual(raw,self.text_asset) @@ -33,4 +33,4 @@ def test_create_command(self): def test_delete_command(self,mock_ok_cancel): self.window.run_command("pieces_delete_asset") yield 500 - self.assertIsNone(AssetSnapshot.assets_snapshot.get(TestCreateAndDeleteCommand.asset_id)) + self.assertIsNone(AssetSnapshot.identifiers_snapshot.get(TestCreateAndDeleteCommand.asset_id)) diff --git a/tests/test_list_assets.py b/tests/test_list_assets.py index c6bb27f..8eceeef 100644 --- a/tests/test_list_assets.py +++ b/tests/test_list_assets.py @@ -24,7 +24,7 @@ def test_assets_list(self): def test_run_command(self): - self.command.run(list(AssetSnapshot.assets_snapshot.keys())[0]) # Open the first asset + self.command.run(list(AssetSnapshot.identifiers_snapshot.keys())[0]) # Open the first asset yield 500 # wait for the command to run sheet_id = list(PiecesListAssetsCommand.sheets_md.keys())[0] # Make sure the correct asset is generate successfully @@ -33,7 +33,7 @@ def test_run_command(self): # Checkout the extracted code code = PiecesListAssetsCommand.sheets_md[sheet_id]["code"] asset_id = PiecesListAssetsCommand.sheets_md[sheet_id]["id"] - raw = AssetSnapshot.assets_snapshot[asset_id].original.reference.fragment.string.raw + raw = AssetSnapshot.identifiers_snapshot[asset_id].original.reference.fragment.string.raw self.assertEqual(code,raw) diff --git a/tests/test_markdown_assets.py b/tests/test_markdown_assets.py index 8619b3e..57d3734 100644 --- a/tests/test_markdown_assets.py +++ b/tests/test_markdown_assets.py @@ -14,7 +14,7 @@ def setUp(self): self.view = self.window.new_file() self.command = PiecesHandleMarkdownCommand(self.window) yield 3000 # Wait 3 sec for everythin to load (websockets) - self.asset_id = list(AssetSnapshot.assets_snapshot.keys())[0] # id to test on + self.asset_id = list(AssetSnapshot.identifiers_snapshot.keys())[0] # id to test on def tearDown(self): @@ -41,7 +41,7 @@ def test_edit_command(self): yield 500 # Wait some until the changes recevied by the websocket - code = AssetSnapshot.assets_snapshot[self.asset_id].original.reference.fragment.string.raw + code = AssetSnapshot.identifiers_snapshot[self.asset_id].original.reference.fragment.string.raw self.assertTrue(code.endswith(self.test_text)) # Make sure the code edited properly diff --git a/tests/test_search.py b/tests/test_search.py index dfe1ce5..3bfe612 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -22,7 +22,7 @@ def test_search_input_handler(self): def test_query_input_handler(self): SearchTypeInputHandler.search_type = "assets" - asset = list(AssetSnapshot.assets_snapshot.values())[0] + asset = list(AssetSnapshot.identifiers_snapshot.values())[0] search_query = asset.name input = QueryInputHandler().next_input({"search_type":SearchTypeInputHandler.search_type,"query":search_query}) items = input.list_items()