develop #3
@@ -1,31 +0,0 @@
|
|||||||
### Note
|
|
||||||
Copy the example and rename it to your desired name. Plugins define a ui target slot with the 'ui_target' requests data but don't have to if not directly interacted with.
|
|
||||||
Plugins must have a run method defined; though, you do not need to necessarily do anything within it. The run method implies that the passed in event system or other data is ready for the plugin to use.
|
|
||||||
|
|
||||||
|
|
||||||
### Manifest Example (All are required even if empty.)
|
|
||||||
```
|
|
||||||
class Manifest:
|
|
||||||
name: str = "Example Plugin"
|
|
||||||
author: str = "John Doe"
|
|
||||||
version: str = "0.0.1"
|
|
||||||
support: str = ""
|
|
||||||
requests: {} = {
|
|
||||||
'pass_ui_objects': ["plugin_control_list"],
|
|
||||||
'pass_events': "true",
|
|
||||||
'bind_keys': []
|
|
||||||
}
|
|
||||||
pre_launch: bool = False
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### Requests
|
|
||||||
```
|
|
||||||
requests: {} = {
|
|
||||||
'pass_events': "true", # If empty or not present will be ignored.
|
|
||||||
"pass_ui_objects": [""], # Request reference to a UI component. Will be passed back as array to plugin.
|
|
||||||
'bind_keys': [f"{name}||send_message:<Control>f"],
|
|
||||||
f"{name}||do_save:<Control>s"] # Bind keys with method and key pare using list. Must pass "name" like shown with delimiter to its right.
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
3
plugins/code/commands/autopairs/__init__.py
Normal file
3
plugins/code/commands/autopairs/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Plugin Module
|
||||||
|
"""
|
||||||
3
plugins/code/commands/autopairs/__main__.py
Normal file
3
plugins/code/commands/autopairs/__main__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Plugin Package
|
||||||
|
"""
|
||||||
97
plugins/code/commands/autopairs/autopairs.py
Normal file
97
plugins/code/commands/autopairs/autopairs.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
# Python imports
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Autopairs:
|
||||||
|
def __init__(self):
|
||||||
|
...
|
||||||
|
|
||||||
|
def handle_word_wrap(self, buffer, char_str: str):
|
||||||
|
wrap_block = self.get_wrap_block(char_str)
|
||||||
|
if not wrap_block: return
|
||||||
|
|
||||||
|
selection = buffer.get_selection_bounds()
|
||||||
|
if not selection:
|
||||||
|
self.insert_pair(buffer, char_str, wrap_block)
|
||||||
|
return True
|
||||||
|
|
||||||
|
self.wrap_selection(buffer, char_str, wrap_block, selection)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def insert_pair(
|
||||||
|
self, buffer, char_str: str, wrap_block: tuple
|
||||||
|
):
|
||||||
|
buffer.begin_user_action()
|
||||||
|
|
||||||
|
left_block, right_block = wrap_block
|
||||||
|
insert_mark = buffer.get_insert()
|
||||||
|
|
||||||
|
insert_itr = buffer.get_iter_at_mark(insert_mark)
|
||||||
|
buffer.insert(insert_itr, f"{left_block}{right_block}")
|
||||||
|
insert_itr = buffer.get_iter_at_mark( insert_mark )
|
||||||
|
insert_itr.backward_char()
|
||||||
|
|
||||||
|
buffer.place_cursor(insert_itr)
|
||||||
|
|
||||||
|
buffer.end_user_action()
|
||||||
|
|
||||||
|
def wrap_selection(
|
||||||
|
self, buffer, char_str: str, wrap_block: tuple, selection
|
||||||
|
):
|
||||||
|
left_block, \
|
||||||
|
right_block = wrap_block
|
||||||
|
start_itr, \
|
||||||
|
end_itr = selection
|
||||||
|
data = buffer.get_text(
|
||||||
|
start_itr, end_itr, include_hidden_chars = False
|
||||||
|
)
|
||||||
|
start_mark = buffer.create_mark("startclose", start_itr, False)
|
||||||
|
end_mark = buffer.create_mark("endclose", end_itr, True)
|
||||||
|
|
||||||
|
buffer.begin_user_action()
|
||||||
|
|
||||||
|
buffer.insert(start_itr, left_block)
|
||||||
|
end_itr = buffer.get_iter_at_mark(end_mark)
|
||||||
|
buffer.insert(end_itr, right_block)
|
||||||
|
|
||||||
|
start = buffer.get_iter_at_mark(start_mark)
|
||||||
|
end = buffer.get_iter_at_mark(end_mark)
|
||||||
|
|
||||||
|
buffer.select_range(start, end)
|
||||||
|
buffer.delete_mark_by_name("startclose")
|
||||||
|
buffer.delete_mark_by_name("endclose")
|
||||||
|
|
||||||
|
buffer.end_user_action()
|
||||||
|
|
||||||
|
def get_wrap_block(self, char_str) -> tuple:
|
||||||
|
left_block = ""
|
||||||
|
right_block = ""
|
||||||
|
|
||||||
|
match char_str:
|
||||||
|
case "(" | ")":
|
||||||
|
left_block = "("
|
||||||
|
right_block = ")"
|
||||||
|
case "[" | "]":
|
||||||
|
left_block = "["
|
||||||
|
right_block = "]"
|
||||||
|
case "{" | "}":
|
||||||
|
left_block = "{"
|
||||||
|
right_block = "}"
|
||||||
|
case '"':
|
||||||
|
left_block = '"'
|
||||||
|
right_block = '"'
|
||||||
|
case "'":
|
||||||
|
left_block = "'"
|
||||||
|
right_block = "'"
|
||||||
|
case "`":
|
||||||
|
left_block = "`"
|
||||||
|
right_block = "`"
|
||||||
|
case _:
|
||||||
|
return ()
|
||||||
|
|
||||||
|
return left_block, right_block
|
||||||
7
plugins/code/commands/autopairs/manifest.json
Normal file
7
plugins/code/commands/autopairs/manifest.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"name": "Autopairs",
|
||||||
|
"author": "ITDominator",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"support": "",
|
||||||
|
"requests": {}
|
||||||
|
}
|
||||||
73
plugins/code/commands/autopairs/plugin.py
Normal file
73
plugins/code/commands/autopairs/plugin.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# Python imports
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from libs.event_factory import Event_Factory, Code_Event_Types
|
||||||
|
|
||||||
|
from plugins.plugin_types import PluginCode
|
||||||
|
|
||||||
|
from .autopairs import Autopairs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
autopairs = Autopairs()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Plugin(PluginCode):
|
||||||
|
def __init__(self):
|
||||||
|
super(Plugin, self).__init__()
|
||||||
|
|
||||||
|
def _controller_message(self, event: Code_Event_Types.CodeEvent):
|
||||||
|
...
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
event = Event_Factory.create_event("register_command",
|
||||||
|
command_name = "autopairs",
|
||||||
|
command = Handler,
|
||||||
|
binding_mode = "held",
|
||||||
|
binding = [
|
||||||
|
"'", "`", "[", "]",
|
||||||
|
'<Shift>"',
|
||||||
|
'<Shift>(',
|
||||||
|
'<Shift>)',
|
||||||
|
'<Shift>{',
|
||||||
|
'<Shift>}'
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.emit_to("source_views", event)
|
||||||
|
|
||||||
|
def unload(self):
|
||||||
|
event = Event_Factory.create_event("unregister_command",
|
||||||
|
command_name = "autopairs",
|
||||||
|
command = Handler,
|
||||||
|
binding_mode = "held",
|
||||||
|
binding = [
|
||||||
|
"'", "`", "[", "]",
|
||||||
|
'<Shift>"',
|
||||||
|
'<Shift>(',
|
||||||
|
'<Shift>)',
|
||||||
|
'<Shift>{',
|
||||||
|
'<Shift>}'
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.emit_to("source_views", event)
|
||||||
|
autopairs = None
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class Handler:
|
||||||
|
@staticmethod
|
||||||
|
def execute(
|
||||||
|
view: any,
|
||||||
|
char_str: str,
|
||||||
|
*args,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
logger.debug("Command: Autopairs")
|
||||||
|
autopairs.handle_word_wrap(view.get_buffer(), char_str)
|
||||||
3
plugins/code/commands/file_history/__init__.py
Normal file
3
plugins/code/commands/file_history/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Plugin Module
|
||||||
|
"""
|
||||||
3
plugins/code/commands/file_history/__main__.py
Normal file
3
plugins/code/commands/file_history/__main__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Plugin Package
|
||||||
|
"""
|
||||||
97
plugins/code/commands/file_history/autopairs.py
Normal file
97
plugins/code/commands/file_history/autopairs.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
# Python imports
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Autopairs:
|
||||||
|
def __init__(self):
|
||||||
|
...
|
||||||
|
|
||||||
|
def handle_word_wrap(self, buffer, char_str: str):
|
||||||
|
wrap_block = self.get_wrap_block(char_str)
|
||||||
|
if not wrap_block: return
|
||||||
|
|
||||||
|
selection = buffer.get_selection_bounds()
|
||||||
|
if not selection:
|
||||||
|
self.insert_pair(buffer, char_str, wrap_block)
|
||||||
|
return True
|
||||||
|
|
||||||
|
self.wrap_selection(buffer, char_str, wrap_block, selection)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def insert_pair(
|
||||||
|
self, buffer, char_str: str, wrap_block: tuple
|
||||||
|
):
|
||||||
|
buffer.begin_user_action()
|
||||||
|
|
||||||
|
left_block, right_block = wrap_block
|
||||||
|
insert_mark = buffer.get_insert()
|
||||||
|
|
||||||
|
insert_itr = buffer.get_iter_at_mark(insert_mark)
|
||||||
|
buffer.insert(insert_itr, f"{left_block}{right_block}")
|
||||||
|
insert_itr = buffer.get_iter_at_mark( insert_mark )
|
||||||
|
insert_itr.backward_char()
|
||||||
|
|
||||||
|
buffer.place_cursor(insert_itr)
|
||||||
|
|
||||||
|
buffer.end_user_action()
|
||||||
|
|
||||||
|
def wrap_selection(
|
||||||
|
self, buffer, char_str: str, wrap_block: tuple, selection
|
||||||
|
):
|
||||||
|
left_block, \
|
||||||
|
right_block = wrap_block
|
||||||
|
start_itr, \
|
||||||
|
end_itr = selection
|
||||||
|
data = buffer.get_text(
|
||||||
|
start_itr, end_itr, include_hidden_chars = False
|
||||||
|
)
|
||||||
|
start_mark = buffer.create_mark("startclose", start_itr, False)
|
||||||
|
end_mark = buffer.create_mark("endclose", end_itr, True)
|
||||||
|
|
||||||
|
buffer.begin_user_action()
|
||||||
|
|
||||||
|
buffer.insert(start_itr, left_block)
|
||||||
|
end_itr = buffer.get_iter_at_mark(end_mark)
|
||||||
|
buffer.insert(end_itr, right_block)
|
||||||
|
|
||||||
|
start = buffer.get_iter_at_mark(start_mark)
|
||||||
|
end = buffer.get_iter_at_mark(end_mark)
|
||||||
|
|
||||||
|
buffer.select_range(start, end)
|
||||||
|
buffer.delete_mark_by_name("startclose")
|
||||||
|
buffer.delete_mark_by_name("endclose")
|
||||||
|
|
||||||
|
buffer.end_user_action()
|
||||||
|
|
||||||
|
def get_wrap_block(self, char_str) -> tuple:
|
||||||
|
left_block = ""
|
||||||
|
right_block = ""
|
||||||
|
|
||||||
|
match char_str:
|
||||||
|
case "(" | ")":
|
||||||
|
left_block = "("
|
||||||
|
right_block = ")"
|
||||||
|
case "[" | "]":
|
||||||
|
left_block = "["
|
||||||
|
right_block = "]"
|
||||||
|
case "{" | "}":
|
||||||
|
left_block = "{"
|
||||||
|
right_block = "}"
|
||||||
|
case '"':
|
||||||
|
left_block = '"'
|
||||||
|
right_block = '"'
|
||||||
|
case "'":
|
||||||
|
left_block = "'"
|
||||||
|
right_block = "'"
|
||||||
|
case "`":
|
||||||
|
left_block = "`"
|
||||||
|
right_block = "`"
|
||||||
|
case _:
|
||||||
|
return ()
|
||||||
|
|
||||||
|
return left_block, right_block
|
||||||
7
plugins/code/commands/file_history/manifest.json
Normal file
7
plugins/code/commands/file_history/manifest.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"name": "File History",
|
||||||
|
"author": "ITDominator",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"support": "",
|
||||||
|
"requests": {}
|
||||||
|
}
|
||||||
65
plugins/code/commands/file_history/plugin.py
Normal file
65
plugins/code/commands/file_history/plugin.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Python imports
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from libs.event_factory import Event_Factory, Code_Event_Types
|
||||||
|
|
||||||
|
from plugins.plugin_types import PluginCode
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
history: list = []
|
||||||
|
history_size: int = 30
|
||||||
|
|
||||||
|
|
||||||
|
class Plugin(PluginCode):
|
||||||
|
def __init__(self):
|
||||||
|
super(Plugin, self).__init__()
|
||||||
|
|
||||||
|
|
||||||
|
def _controller_message(self, event: Code_Event_Types.CodeEvent):
|
||||||
|
if isinstance(event, Code_Event_Types.RemovedFileEvent):
|
||||||
|
if event.file.ftype == "buffer": return
|
||||||
|
|
||||||
|
if len(history) == history_size:
|
||||||
|
history.pop(0)
|
||||||
|
|
||||||
|
history.append(event.file)
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
self._manage_signals("register_command")
|
||||||
|
|
||||||
|
def unload(self):
|
||||||
|
self._manage_signals("unregister_command")
|
||||||
|
|
||||||
|
def _manage_signals(self, action: str):
|
||||||
|
event = Event_Factory.create_event(action,
|
||||||
|
command_name = "file_history_pop",
|
||||||
|
command = Handler,
|
||||||
|
binding_mode = "released",
|
||||||
|
binding = "<Shift><Control>t"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.emit_to("source_views", event)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class Handler:
|
||||||
|
@staticmethod
|
||||||
|
def execute(
|
||||||
|
view: any,
|
||||||
|
char_str: str,
|
||||||
|
*args,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
logger.debug("Command: File History")
|
||||||
|
if len(history) == 0: return
|
||||||
|
|
||||||
|
view._on_uri_data_received(
|
||||||
|
[
|
||||||
|
history.pop().replace("file://", "")
|
||||||
|
]
|
||||||
|
)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Python imports
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version('GtkSource', '4')
|
||||||
|
|
||||||
|
from gi.repository import GtkSource
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from .helpers import clear_temp_cut_buffer_delayed, set_temp_cut_buffer_delayed
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Handler:
|
||||||
|
@staticmethod
|
||||||
|
def execute(
|
||||||
|
view: GtkSource.View,
|
||||||
|
*args,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
logger.debug("Command: Cut to Temp Buffer")
|
||||||
|
|
||||||
|
clear_temp_cut_buffer_delayed(view)
|
||||||
|
|
||||||
|
buffer = view.get_buffer()
|
||||||
|
itr = buffer.get_iter_at_mark(buffer.get_insert())
|
||||||
|
|
||||||
|
start_itr = itr.copy()
|
||||||
|
start_itr.set_line_offset(0)
|
||||||
|
|
||||||
|
end_itr = start_itr.copy()
|
||||||
|
if not end_itr.forward_line():
|
||||||
|
end_itr = buffer.get_end_iter()
|
||||||
|
|
||||||
|
if not hasattr(view, "_cut_buffer"):
|
||||||
|
view._cut_buffer = ""
|
||||||
|
|
||||||
|
line_str = buffer.get_text(start_itr, end_itr, True)
|
||||||
|
view._cut_buffer += line_str
|
||||||
|
|
||||||
|
buffer.delete(start_itr, end_itr)
|
||||||
|
|
||||||
|
set_temp_cut_buffer_delayed(view)
|
||||||
24
plugins/code/commands/nanoesq_temp_buffer/helpers.py
Normal file
24
plugins/code/commands/nanoesq_temp_buffer/helpers.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Python imports
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
import gi
|
||||||
|
|
||||||
|
from gi.repository import GLib
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def clear_temp_cut_buffer_delayed(view: any):
|
||||||
|
if not hasattr(view, "_cut_temp_timeout_id"): return
|
||||||
|
if not view._cut_temp_timeout_id: return
|
||||||
|
|
||||||
|
GLib.source_remove(view._cut_temp_timeout_id)
|
||||||
|
|
||||||
|
def set_temp_cut_buffer_delayed(view: any):
|
||||||
|
def clear_temp_buffer(view: any):
|
||||||
|
view._cut_buffer = ""
|
||||||
|
view._cut_temp_timeout_id = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
view._cut_temp_timeout_id = GLib.timeout_add(15000, clear_temp_buffer, view)
|
||||||
7
plugins/code/commands/nanoesq_temp_buffer/manifest.json
Normal file
7
plugins/code/commands/nanoesq_temp_buffer/manifest.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"name": "Nanoesq Temp Buffer",
|
||||||
|
"author": "ITDominator",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"support": "",
|
||||||
|
"requests": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Python imports
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version('GtkSource', '4')
|
||||||
|
|
||||||
|
from gi.repository import GLib
|
||||||
|
from gi.repository import GtkSource
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from .helpers import clear_temp_cut_buffer_delayed, set_temp_cut_buffer_delayed
|
||||||
|
|
||||||
|
|
||||||
|
class Handler2:
|
||||||
|
@staticmethod
|
||||||
|
def execute(
|
||||||
|
view: GtkSource.View,
|
||||||
|
*args,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
logger.debug("Command: Paste Temp Buffer")
|
||||||
|
if not hasattr(view, "_cut_temp_timeout_id"): return
|
||||||
|
if not hasattr(view, "_cut_buffer"): return
|
||||||
|
if not view._cut_buffer: return
|
||||||
|
|
||||||
|
clear_temp_cut_buffer_delayed(view)
|
||||||
|
|
||||||
|
buffer = view.get_buffer()
|
||||||
|
itr = buffer.get_iter_at_mark( buffer.get_insert() )
|
||||||
|
insert_itr = itr.copy()
|
||||||
|
|
||||||
|
buffer.insert(insert_itr, view._cut_buffer, -1)
|
||||||
|
|
||||||
|
set_temp_cut_buffer_delayed(view)
|
||||||
49
plugins/code/commands/nanoesq_temp_buffer/plugin.py
Normal file
49
plugins/code/commands/nanoesq_temp_buffer/plugin.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Python imports
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from libs.event_factory import Event_Factory, Code_Event_Types
|
||||||
|
|
||||||
|
from plugins.plugin_types import PluginCode
|
||||||
|
|
||||||
|
from .cut_to_temp_buffer import Handler
|
||||||
|
from .paste_temp_buffer import Handler2
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Plugin(PluginCode):
|
||||||
|
def __init__(self):
|
||||||
|
super(Plugin, self).__init__()
|
||||||
|
|
||||||
|
|
||||||
|
def _controller_message(self, event: Code_Event_Types.CodeEvent):
|
||||||
|
...
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
self._manage_signals("register_command")
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
self._manage_signals("unregister_command")
|
||||||
|
|
||||||
|
def _manage_signals(self, action: str):
|
||||||
|
event = Event_Factory.create_event(action,
|
||||||
|
command_name = "cut_to_temp_buffer",
|
||||||
|
command = Handler,
|
||||||
|
binding_mode = "held",
|
||||||
|
binding = "<Control>k"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.emit_to("source_views", event)
|
||||||
|
|
||||||
|
event = Event_Factory.create_event(action,
|
||||||
|
command_name = "paste_temp_buffer",
|
||||||
|
command = Handler2,
|
||||||
|
binding_mode = "held",
|
||||||
|
binding = "<Control>u"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.emit_to("source_views", event)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
...
|
||||||
3
plugins/code/commands/toggle_source_view/__init__.py
Normal file
3
plugins/code/commands/toggle_source_view/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Plugin Module
|
||||||
|
"""
|
||||||
3
plugins/code/commands/toggle_source_view/__main__.py
Normal file
3
plugins/code/commands/toggle_source_view/__main__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Plugin Package
|
||||||
|
"""
|
||||||
7
plugins/code/commands/toggle_source_view/manifest.json
Normal file
7
plugins/code/commands/toggle_source_view/manifest.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"name": "Toggle Source View",
|
||||||
|
"author": "ITDominator",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"support": "",
|
||||||
|
"requests": {}
|
||||||
|
}
|
||||||
64
plugins/code/commands/toggle_source_view/plugin.py
Normal file
64
plugins/code/commands/toggle_source_view/plugin.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# Python imports
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from libs.event_factory import Event_Factory, Code_Event_Types
|
||||||
|
|
||||||
|
from plugins.plugin_types import PluginCode
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Plugin(PluginCode):
|
||||||
|
def __init__(self):
|
||||||
|
super(Plugin, self).__init__()
|
||||||
|
|
||||||
|
def _controller_message(self, event: Code_Event_Types.CodeEvent):
|
||||||
|
...
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
event = Event_Factory.create_event("register_command",
|
||||||
|
command_name = "toggle_source_view",
|
||||||
|
command = Handler,
|
||||||
|
binding_mode = "released",
|
||||||
|
binding = "<Shift><Control>h"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.emit_to("source_views", event)
|
||||||
|
|
||||||
|
def unload(self):
|
||||||
|
event = Event_Factory.create_event("unregister_command",
|
||||||
|
command_name = "toggle_source_view",
|
||||||
|
command = Handler,
|
||||||
|
binding_mode = "released",
|
||||||
|
binding = "<Shift><Control>h"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.emit_to("source_views", event)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class Handler:
|
||||||
|
@staticmethod
|
||||||
|
def execute(
|
||||||
|
view: any,
|
||||||
|
char_str: str,
|
||||||
|
*args,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
logger.debug("Command: Toggle Source View")
|
||||||
|
target = view.get_parent()
|
||||||
|
target.hide() if target.is_visible() else target.show()
|
||||||
|
|
||||||
|
if view.sibling_left:
|
||||||
|
target = view.sibling_left.get_parent()
|
||||||
|
target.show()
|
||||||
|
view.sibling_left.grab_focus()
|
||||||
|
|
||||||
|
if view.sibling_right:
|
||||||
|
target = view.sibling_right.get_parent()
|
||||||
|
target.show()
|
||||||
|
view.sibling_right.grab_focus()
|
||||||
|
|
||||||
3
plugins/code/completers/example_completer/__init__.py
Normal file
3
plugins/code/completers/example_completer/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Pligin Module
|
||||||
|
"""
|
||||||
3
plugins/code/completers/example_completer/__main__.py
Normal file
3
plugins/code/completers/example_completer/__main__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Pligin Package
|
||||||
|
"""
|
||||||
7
plugins/code/completers/example_completer/manifest.json
Normal file
7
plugins/code/completers/example_completer/manifest.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"name": "Example Completer",
|
||||||
|
"author": "John Doe",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"support": "",
|
||||||
|
"requests": {}
|
||||||
|
}
|
||||||
49
plugins/code/completers/example_completer/plugin.py
Normal file
49
plugins/code/completers/example_completer/plugin.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Python imports
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
import gi
|
||||||
|
gi.require_version('Gtk', '3.0')
|
||||||
|
from gi.repository import Gtk
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from libs.event_factory import Event_Factory, Code_Event_Types
|
||||||
|
|
||||||
|
from plugins.plugin_types import PluginCode
|
||||||
|
|
||||||
|
from .provider import Provider
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Plugin(PluginCode):
|
||||||
|
def __init__(self):
|
||||||
|
super(Plugin, self).__init__()
|
||||||
|
|
||||||
|
self.provider: Provider = None
|
||||||
|
|
||||||
|
|
||||||
|
def _controller_message(self, event: Code_Event_Types.CodeEvent):
|
||||||
|
...
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
self.provider = Provider()
|
||||||
|
|
||||||
|
event = Event_Factory.create_event(
|
||||||
|
"register_provider",
|
||||||
|
provider_name = "Example Completer",
|
||||||
|
provider = self.provider,
|
||||||
|
language_ids = []
|
||||||
|
)
|
||||||
|
self.emit_to("completion", event)
|
||||||
|
|
||||||
|
def unload(self):
|
||||||
|
event = Event_Factory.create_event(
|
||||||
|
"unregister_provider",
|
||||||
|
provider_name = "Example Completer"
|
||||||
|
)
|
||||||
|
self.emit_to("completion", event)
|
||||||
|
|
||||||
|
self.provider = None
|
||||||
|
del self.provider
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
...
|
||||||
76
plugins/code/completers/example_completer/provider.py
Normal file
76
plugins/code/completers/example_completer/provider.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# Python imports
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
import gi
|
||||||
|
gi.require_version('GtkSource', '4')
|
||||||
|
|
||||||
|
from gi.repository import GObject
|
||||||
|
from gi.repository import GtkSource
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from .provider_response_cache import ProviderResponseCache
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Provider(GObject.GObject, GtkSource.CompletionProvider):
|
||||||
|
"""
|
||||||
|
This is a custom Completion Example Provider.
|
||||||
|
# NOTE: used information from here --> https://warroom.rsmus.com/do-that-auto-complete/
|
||||||
|
"""
|
||||||
|
__gtype_name__ = 'ExampleCompletionProvider'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(Provider, self).__init__()
|
||||||
|
|
||||||
|
self.response_cache: ProviderResponseCache = ProviderResponseCache()
|
||||||
|
|
||||||
|
|
||||||
|
def do_get_name(self):
|
||||||
|
""" Returns: a new string containing the name of the provider. """
|
||||||
|
return 'Example Code Completion'
|
||||||
|
|
||||||
|
def do_match(self, context):
|
||||||
|
# Note: If provider is in interactive activation then need to check
|
||||||
|
# view focus as otherwise non focus views start trying to grab it.
|
||||||
|
completion = context.get_property("completion")
|
||||||
|
if not completion.get_view().has_focus(): return
|
||||||
|
|
||||||
|
word = self.response_cache.get_word(context)
|
||||||
|
if not word or len(word) < 2: return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def do_get_priority(self):
|
||||||
|
""" Determin position in result list along other providor results. """
|
||||||
|
return 5
|
||||||
|
|
||||||
|
def do_activate_proposal(self, proposal, iter_):
|
||||||
|
""" Manually handle actual completion insert or set flags and handle normally. """
|
||||||
|
|
||||||
|
buffer = iter_.get_buffer()
|
||||||
|
# Note: Flag mostly intended for SourceViewsMultiInsertState
|
||||||
|
# to insure marker processes inserted text correctly.
|
||||||
|
buffer.is_processing_completion = True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def do_get_activation(self):
|
||||||
|
""" The context for when a provider will show results """
|
||||||
|
# return GtkSource.CompletionActivation.NONE
|
||||||
|
# return GtkSource.CompletionActivation.USER_REQUESTED
|
||||||
|
# return GtkSource.CompletionActivation.USER_REQUESTED | GtkSource.CompletionActivation.INTERACTIVE
|
||||||
|
return GtkSource.CompletionActivation.INTERACTIVE
|
||||||
|
|
||||||
|
def do_populate(self, context):
|
||||||
|
results = self.response_cache.filter_with_context(context)
|
||||||
|
proposals = []
|
||||||
|
|
||||||
|
for entry in results:
|
||||||
|
proposals.append(
|
||||||
|
self.response_cache.create_completion_item(
|
||||||
|
entry["label"],
|
||||||
|
entry["text"],
|
||||||
|
entry["info"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
context.add_proposals(self, proposals, True)
|
||||||
|
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# Python imports
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
import re
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
import gi
|
||||||
|
gi.require_version('Gtk', '3.0')
|
||||||
|
gi.require_version('GtkSource', '4')
|
||||||
|
|
||||||
|
from gi.repository import Gtk
|
||||||
|
from gi.repository import GLib
|
||||||
|
from gi.repository import GtkSource
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from libs.event_factory import Code_Event_Types
|
||||||
|
|
||||||
|
from core.widgets.code.completion_providers.provider_response_cache_base import ProviderResponseCacheBase
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderResponseCache(ProviderResponseCacheBase):
|
||||||
|
def __init__(self):
|
||||||
|
super(ProviderResponseCache, self).__init__()
|
||||||
|
|
||||||
|
# Note: Using asyncio.run causes a keyboard trap that prevents app
|
||||||
|
# closure from terminal. ThreadPoolExecutor seems to not have such issues...
|
||||||
|
self.executor = ThreadPoolExecutor(max_workers = 1)
|
||||||
|
self.matchers: dict = {
|
||||||
|
"hello": {
|
||||||
|
"label": "Hello, World!",
|
||||||
|
"text": "Hello, World!",
|
||||||
|
"info": GLib.markup_escape_text( "<b>Says the first ever program developers write...</b>" )
|
||||||
|
},
|
||||||
|
"foo": {
|
||||||
|
"label": "foo",
|
||||||
|
"text": "foo }}"
|
||||||
|
},
|
||||||
|
"bar": {
|
||||||
|
"label": "bar",
|
||||||
|
"text": "bar }}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def process_file_load(self, event: Code_Event_Types.AddedNewFileEvent):
|
||||||
|
...
|
||||||
|
|
||||||
|
def process_file_close(self, event: Code_Event_Types.RemovedFileEvent):
|
||||||
|
...
|
||||||
|
|
||||||
|
def process_file_save(self, event: Code_Event_Types.SavedFileEvent):
|
||||||
|
...
|
||||||
|
|
||||||
|
def process_file_change(self, event: Code_Event_Types.TextChangedEvent):
|
||||||
|
...
|
||||||
|
|
||||||
|
def filter(self, word: str) -> list[dict]:
|
||||||
|
...
|
||||||
|
|
||||||
|
def filter_with_context(self, context) -> list[dict]:
|
||||||
|
"""
|
||||||
|
In this instance, it will do 2 things:
|
||||||
|
1) always provide Hello World! (Not ideal but an option so its in the example)
|
||||||
|
2) Utilizes the Gtk.TextIter from the TextBuffer to determine if there is a jinja
|
||||||
|
example of '{{ custom.' if so it will provide you with the options of foo and bar.
|
||||||
|
If selected it will insert foo }} or bar }}, completing your syntax...
|
||||||
|
|
||||||
|
PLEASE NOTE the GtkTextIter Logic and regex are really rough and should be adjusted and tuned
|
||||||
|
"""
|
||||||
|
|
||||||
|
proposals: list[dict] = [
|
||||||
|
{
|
||||||
|
"label": self.matchers[ "hello" ]["label"],
|
||||||
|
"text": self.matchers[ "hello" ]["text"],
|
||||||
|
"info": self.matchers[ "hello" ]["info"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Gtk Versions differ on get_iter responses...
|
||||||
|
end_iter = context.get_iter()
|
||||||
|
if not isinstance(end_iter, Gtk.TextIter):
|
||||||
|
_, end_iter = context.get_iter()
|
||||||
|
|
||||||
|
if not end_iter: return
|
||||||
|
|
||||||
|
buf = end_iter.get_buffer()
|
||||||
|
mov_iter = end_iter.copy()
|
||||||
|
if mov_iter.backward_search('{{', Gtk.TextSearchFlags.VISIBLE_ONLY):
|
||||||
|
mov_iter, _ = mov_iter.backward_search('{{', Gtk.TextSearchFlags.VISIBLE_ONLY)
|
||||||
|
left_text = buf.get_text(mov_iter, end_iter, True)
|
||||||
|
else:
|
||||||
|
left_text = ''
|
||||||
|
|
||||||
|
if re.match(r'.*\{\{\s*custom\.$', left_text):
|
||||||
|
# optionally proposed based on left search via regex
|
||||||
|
proposals.append(
|
||||||
|
{
|
||||||
|
"label": self.matchers[ "foo" ]["label"],
|
||||||
|
"text": self.matchers[ "foo" ]["text"],
|
||||||
|
"info": ""
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# optionally proposed based on left search via regex
|
||||||
|
proposals.append(
|
||||||
|
{
|
||||||
|
"label": self.matchers[ "bar" ]["label"],
|
||||||
|
"text": self.matchers[ "bar" ]["text"],
|
||||||
|
"info": ""
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return proposals
|
||||||
3
plugins/code/completers/snippets_completer/__init__.py
Normal file
3
plugins/code/completers/snippets_completer/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Pligin Module
|
||||||
|
"""
|
||||||
3
plugins/code/completers/snippets_completer/__main__.py
Normal file
3
plugins/code/completers/snippets_completer/__main__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Pligin Package
|
||||||
|
"""
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from .parser import load, loads
|
||||||
|
from .writer import dump, dumps
|
||||||
|
from .speg import ParseError
|
||||||
295
plugins/code/completers/snippets_completer/cson/parser.py
Normal file
295
plugins/code/completers/snippets_completer/cson/parser.py
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
from .speg import peg
|
||||||
|
import re, sys
|
||||||
|
|
||||||
|
if sys.version_info[0] == 2:
|
||||||
|
_chr = unichr
|
||||||
|
else:
|
||||||
|
_chr = chr
|
||||||
|
|
||||||
|
def load(fin):
|
||||||
|
return loads(fin.read())
|
||||||
|
|
||||||
|
def loads(s):
|
||||||
|
if isinstance(s, bytes):
|
||||||
|
s = s.decode('utf-8')
|
||||||
|
if s.startswith(u'\ufeff'):
|
||||||
|
s = s[1:]
|
||||||
|
return peg(s.replace('\r\n', '\n'), _p_root)
|
||||||
|
|
||||||
|
def _p_ws(p):
|
||||||
|
p('[ \t]*')
|
||||||
|
|
||||||
|
def _p_nl(p):
|
||||||
|
p(r'([ \t]*(?:#[^\n]*)?\r?\n)+')
|
||||||
|
|
||||||
|
def _p_ews(p):
|
||||||
|
with p:
|
||||||
|
p(_p_nl)
|
||||||
|
p(_p_ws)
|
||||||
|
|
||||||
|
def _p_id(p):
|
||||||
|
return p(r'[$a-zA-Z_][$0-9a-zA-Z_]*')
|
||||||
|
|
||||||
|
_escape_table = {
|
||||||
|
'r': '\r',
|
||||||
|
'n': '\n',
|
||||||
|
't': '\t',
|
||||||
|
'f': '\f',
|
||||||
|
'b': '\b',
|
||||||
|
}
|
||||||
|
def _p_unescape(p):
|
||||||
|
esc = p('\\\\(?:u[0-9a-fA-F]{4}|[^\n])')
|
||||||
|
if esc[1] == 'u':
|
||||||
|
return _chr(int(esc[2:], 16))
|
||||||
|
return _escape_table.get(esc[1:], esc[1:])
|
||||||
|
|
||||||
|
_re_indent = re.compile(r'[ \t]*')
|
||||||
|
def _p_block_str(p, c):
|
||||||
|
p(r'{c}{c}{c}'.format(c=c))
|
||||||
|
lines = [['']]
|
||||||
|
with p:
|
||||||
|
while True:
|
||||||
|
s = p(r'(?:{c}(?!{c}{c})|[^{c}\\])*'.format(c=c))
|
||||||
|
l = s.split('\n')
|
||||||
|
lines[-1].append(l[0])
|
||||||
|
lines.extend([x] for x in l[1:])
|
||||||
|
if p(r'(?:\\\n[ \t]*)*'):
|
||||||
|
continue
|
||||||
|
p.commit()
|
||||||
|
lines[-1].append(p(_p_unescape))
|
||||||
|
p(r'{c}{c}{c}'.format(c=c))
|
||||||
|
|
||||||
|
lines = [''.join(l) for l in lines]
|
||||||
|
strip_ws = len(lines) > 1
|
||||||
|
if strip_ws and all(c in ' \t' for c in lines[-1]):
|
||||||
|
lines.pop()
|
||||||
|
|
||||||
|
indent = None
|
||||||
|
for line in lines[1:]:
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
if indent is None:
|
||||||
|
indent = _re_indent.match(line).group(0)
|
||||||
|
continue
|
||||||
|
for i, (c1, c2) in enumerate(zip(indent, line)):
|
||||||
|
if c1 != c2:
|
||||||
|
indent = indent[:i]
|
||||||
|
break
|
||||||
|
|
||||||
|
ind_len = len(indent or '')
|
||||||
|
if strip_ws and all(c in ' \t' for c in lines[0]):
|
||||||
|
lines = [line[ind_len:] for line in lines[1:]]
|
||||||
|
else:
|
||||||
|
lines[1:] = [line[ind_len:] for line in lines[1:]]
|
||||||
|
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
_re_mstr_nl = re.compile(r'(?:[ \t]*\n)+[ \t]*')
|
||||||
|
_re_mstr_trailing_nl = re.compile(_re_mstr_nl.pattern + r'\Z')
|
||||||
|
def _p_multiline_str(p, c):
|
||||||
|
p('{c}(?!{c}{c})(?:[ \t]*\n[ \t]*)?'.format(c=c))
|
||||||
|
string_parts = []
|
||||||
|
with p:
|
||||||
|
while True:
|
||||||
|
string_parts.append(p(r'[^{c}\\]*'.format(c=c)))
|
||||||
|
if p(r'(?:\\\n[ \t]*)*'):
|
||||||
|
string_parts.append('')
|
||||||
|
continue
|
||||||
|
p.commit()
|
||||||
|
string_parts.append(p(_p_unescape))
|
||||||
|
p(c)
|
||||||
|
string_parts[-1] = _re_mstr_trailing_nl.sub('', string_parts[-1])
|
||||||
|
string_parts[::2] = [_re_mstr_nl.sub(' ', part) for part in string_parts[::2]]
|
||||||
|
return ''.join(string_parts)
|
||||||
|
|
||||||
|
def _p_string(p):
|
||||||
|
with p:
|
||||||
|
return p(_p_block_str, '"')
|
||||||
|
with p:
|
||||||
|
return p(_p_block_str, "'")
|
||||||
|
with p:
|
||||||
|
return p(_p_multiline_str, '"')
|
||||||
|
return p(_p_multiline_str, "'")
|
||||||
|
|
||||||
|
def _p_array_value(p):
|
||||||
|
with p:
|
||||||
|
p(_p_nl)
|
||||||
|
return p(_p_object)
|
||||||
|
with p:
|
||||||
|
p(_p_ws)
|
||||||
|
return p(_p_line_object)
|
||||||
|
p(_p_ews)
|
||||||
|
return p(_p_simple_value)
|
||||||
|
|
||||||
|
def _p_key(p):
|
||||||
|
with p:
|
||||||
|
return p(_p_id)
|
||||||
|
return p(_p_string)
|
||||||
|
|
||||||
|
def _p_flow_kv(p):
|
||||||
|
k = p(_p_key)
|
||||||
|
p(_p_ews)
|
||||||
|
p(':')
|
||||||
|
with p:
|
||||||
|
p(_p_nl)
|
||||||
|
return k, p(_p_object)
|
||||||
|
with p:
|
||||||
|
p(_p_ws)
|
||||||
|
return k, p(_p_line_object)
|
||||||
|
p(_p_ews)
|
||||||
|
return k, p(_p_simple_value)
|
||||||
|
|
||||||
|
def _p_flow_obj_sep(p):
|
||||||
|
with p:
|
||||||
|
p(_p_ews)
|
||||||
|
p(',')
|
||||||
|
p(_p_ews)
|
||||||
|
return
|
||||||
|
|
||||||
|
p(_p_nl)
|
||||||
|
p(_p_ws)
|
||||||
|
|
||||||
|
def _p_simple_value(p):
|
||||||
|
with p:
|
||||||
|
p('null')
|
||||||
|
return None
|
||||||
|
|
||||||
|
with p:
|
||||||
|
p('false')
|
||||||
|
return False
|
||||||
|
with p:
|
||||||
|
p('true')
|
||||||
|
return True
|
||||||
|
|
||||||
|
with p:
|
||||||
|
return int(p('0b[01]+')[2:], 2)
|
||||||
|
with p:
|
||||||
|
return int(p('0o[0-7]+')[2:], 8)
|
||||||
|
with p:
|
||||||
|
return int(p('0x[0-9a-fA-F]+')[2:], 16)
|
||||||
|
with p:
|
||||||
|
return float(p(r'-?(?:[1-9][0-9]*|0)?\.[0-9]+(?:[Ee][\+-]?[0-9]+)?|(?:[1-9][0-9]*|0)(?:\.[0-9]+)?[Ee][\+-]?[0-9]+'))
|
||||||
|
with p:
|
||||||
|
return int(p('-?[1-9][0-9]*|0'), 10)
|
||||||
|
|
||||||
|
with p:
|
||||||
|
return p(_p_string)
|
||||||
|
|
||||||
|
with p:
|
||||||
|
p(r'\[')
|
||||||
|
r = []
|
||||||
|
with p:
|
||||||
|
p.set('I', '')
|
||||||
|
r.append(p(_p_array_value))
|
||||||
|
with p:
|
||||||
|
while True:
|
||||||
|
with p:
|
||||||
|
p(_p_ews)
|
||||||
|
p(',')
|
||||||
|
rr = p(_p_array_value)
|
||||||
|
if not p:
|
||||||
|
p(_p_nl)
|
||||||
|
with p:
|
||||||
|
rr = p(_p_object)
|
||||||
|
if not p:
|
||||||
|
p(_p_ews)
|
||||||
|
rr = p(_p_simple_value)
|
||||||
|
r.append(rr)
|
||||||
|
p.commit()
|
||||||
|
with p:
|
||||||
|
p(_p_ews)
|
||||||
|
p(',')
|
||||||
|
p(_p_ews)
|
||||||
|
p(r'\]')
|
||||||
|
return r
|
||||||
|
|
||||||
|
p(r'\{')
|
||||||
|
|
||||||
|
r = {}
|
||||||
|
p(_p_ews)
|
||||||
|
with p:
|
||||||
|
p.set('I', '')
|
||||||
|
k, v = p(_p_flow_kv)
|
||||||
|
r[k] = v
|
||||||
|
with p:
|
||||||
|
while True:
|
||||||
|
p(_p_flow_obj_sep)
|
||||||
|
k, v = p(_p_flow_kv)
|
||||||
|
r[k] = v
|
||||||
|
p.commit()
|
||||||
|
p(_p_ews)
|
||||||
|
with p:
|
||||||
|
p(',')
|
||||||
|
p(_p_ews)
|
||||||
|
p(r'\}')
|
||||||
|
return r
|
||||||
|
|
||||||
|
def _p_line_kv(p):
|
||||||
|
k = p(_p_key)
|
||||||
|
p(_p_ws)
|
||||||
|
p(':')
|
||||||
|
p(_p_ws)
|
||||||
|
with p:
|
||||||
|
p(_p_nl)
|
||||||
|
p(p.get('I'))
|
||||||
|
return k, p(_p_indented_object)
|
||||||
|
with p:
|
||||||
|
return k, p(_p_line_object)
|
||||||
|
with p:
|
||||||
|
return k, p(_p_simple_value)
|
||||||
|
p(_p_nl)
|
||||||
|
p(p.get('I'))
|
||||||
|
p('[ \t]')
|
||||||
|
p(_p_ws)
|
||||||
|
return k, p(_p_simple_value)
|
||||||
|
|
||||||
|
def _p_line_object(p):
|
||||||
|
k, v = p(_p_line_kv)
|
||||||
|
r = { k: v }
|
||||||
|
with p:
|
||||||
|
while True:
|
||||||
|
p(_p_ws)
|
||||||
|
p(',')
|
||||||
|
p(_p_ws)
|
||||||
|
k, v = p(_p_line_kv)
|
||||||
|
r[k] = v # uniqueness
|
||||||
|
p.commit()
|
||||||
|
return r
|
||||||
|
|
||||||
|
def _p_object(p):
|
||||||
|
p.set('I', p.get('I') + p('[ \t]*'))
|
||||||
|
r = p(_p_line_object)
|
||||||
|
with p:
|
||||||
|
while True:
|
||||||
|
p(_p_ws)
|
||||||
|
with p:
|
||||||
|
p(',')
|
||||||
|
p(_p_nl)
|
||||||
|
p(p.get('I'))
|
||||||
|
rr = p(_p_line_object)
|
||||||
|
r.update(rr) # unqueness
|
||||||
|
p.commit()
|
||||||
|
return r
|
||||||
|
|
||||||
|
def _p_indented_object(p):
|
||||||
|
p.set('I', p.get('I') + p('[ \t]'))
|
||||||
|
return p(_p_object)
|
||||||
|
|
||||||
|
def _p_root(p):
|
||||||
|
with p:
|
||||||
|
p(_p_nl)
|
||||||
|
|
||||||
|
with p:
|
||||||
|
p.set('I', '')
|
||||||
|
r = p(_p_object)
|
||||||
|
p(_p_ws)
|
||||||
|
with p:
|
||||||
|
p(',')
|
||||||
|
|
||||||
|
if not p:
|
||||||
|
p(_p_ws)
|
||||||
|
r = p(_p_simple_value)
|
||||||
|
|
||||||
|
p(_p_ews)
|
||||||
|
p(p.eof)
|
||||||
|
return r
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
from .peg import peg, ParseError
|
||||||
147
plugins/code/completers/snippets_completer/cson/speg/peg.py
Normal file
147
plugins/code/completers/snippets_completer/cson/speg/peg.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import sys, re
|
||||||
|
|
||||||
|
class ParseError(Exception):
|
||||||
|
def __init__(self, msg, text, offset, line, col):
|
||||||
|
self.msg = msg
|
||||||
|
self.text = text
|
||||||
|
self.offset = offset
|
||||||
|
self.line = line
|
||||||
|
self.col = col
|
||||||
|
super(ParseError, self).__init__(msg, offset, line, col)
|
||||||
|
|
||||||
|
if sys.version_info[0] == 2:
|
||||||
|
_basestr = basestring
|
||||||
|
else:
|
||||||
|
_basestr = str
|
||||||
|
|
||||||
|
def peg(s, r):
|
||||||
|
p = _Peg(s)
|
||||||
|
try:
|
||||||
|
return p(r)
|
||||||
|
except _UnexpectedError as e:
|
||||||
|
offset = max(p._errors)
|
||||||
|
err = p._errors[offset]
|
||||||
|
raise ParseError(err.msg, s, offset, err.line, err.col)
|
||||||
|
|
||||||
|
class _UnexpectedError(RuntimeError):
|
||||||
|
def __init__(self, state, expr):
|
||||||
|
self.state = state
|
||||||
|
self.expr = expr
|
||||||
|
|
||||||
|
class _PegState:
|
||||||
|
def __init__(self, pos, line, col):
|
||||||
|
self.pos = pos
|
||||||
|
self.line = line
|
||||||
|
self.col = col
|
||||||
|
self.vars = {}
|
||||||
|
self.commited = False
|
||||||
|
|
||||||
|
class _PegError:
|
||||||
|
def __init__(self, msg, line, col):
|
||||||
|
self.msg = msg
|
||||||
|
self.line = line
|
||||||
|
self.col = col
|
||||||
|
|
||||||
|
class _Peg:
|
||||||
|
def __init__(self, s):
|
||||||
|
self._s = s
|
||||||
|
self._states = [_PegState(0, 1, 1)]
|
||||||
|
self._errors = {}
|
||||||
|
self._re_cache = {}
|
||||||
|
|
||||||
|
def __call__(self, r, *args, **kw):
|
||||||
|
if isinstance(r, _basestr):
|
||||||
|
compiled = self._re_cache.get(r)
|
||||||
|
if not compiled:
|
||||||
|
compiled = re.compile(r)
|
||||||
|
self._re_cache[r] = compiled
|
||||||
|
st = self._states[-1]
|
||||||
|
m = compiled.match(self._s[st.pos:])
|
||||||
|
if not m:
|
||||||
|
self.error(expr=r, err=kw.get('err'))
|
||||||
|
|
||||||
|
ms = m.group(0)
|
||||||
|
st.pos += len(ms)
|
||||||
|
nl_pos = ms.rfind('\n')
|
||||||
|
if nl_pos < 0:
|
||||||
|
st.col += len(ms)
|
||||||
|
else:
|
||||||
|
st.col = len(ms) - nl_pos
|
||||||
|
st.line += ms[:nl_pos].count('\n') + 1
|
||||||
|
return ms
|
||||||
|
else:
|
||||||
|
kw.pop('err', None)
|
||||||
|
return r(self, *args, **kw)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
pos = self._states[-1].pos
|
||||||
|
vars = {}
|
||||||
|
for st in self._states:
|
||||||
|
vars.update(st.vars)
|
||||||
|
return '_Peg(%r, %r)' % (self._s[:pos] + '*' + self._s[pos:], vars)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def eof(p):
|
||||||
|
if p._states[-1].pos != len(p._s):
|
||||||
|
p.error()
|
||||||
|
|
||||||
|
def error(self, err=None, expr=None):
|
||||||
|
st = self._states[-1]
|
||||||
|
if err is None:
|
||||||
|
err = 'expected {!r}, found {!r}'.format(expr, self._s[st.pos:st.pos+4])
|
||||||
|
self._errors[st.pos] = _PegError(err, st.line, st.col)
|
||||||
|
raise _UnexpectedError(st, expr)
|
||||||
|
|
||||||
|
def get(self, key, default=None):
|
||||||
|
for state in self._states[::-1]:
|
||||||
|
if key in state.vars:
|
||||||
|
return state.vars[key][0]
|
||||||
|
return default
|
||||||
|
|
||||||
|
def set(self, key, value):
|
||||||
|
self._states[-1].vars[key] = value, False
|
||||||
|
|
||||||
|
def set_global(self, key, value):
|
||||||
|
self._states[-1].vars[key] = value, True
|
||||||
|
|
||||||
|
def opt(self, *args, **kw):
|
||||||
|
with self:
|
||||||
|
return self(*args, **kw)
|
||||||
|
|
||||||
|
def not_(self, s, *args, **kw):
|
||||||
|
with self:
|
||||||
|
self(s)
|
||||||
|
self.error()
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self._states[-1].committed = False
|
||||||
|
self._states.append(_PegState(self._states[-1].pos, self._states[-1].line, self._states[-1].col))
|
||||||
|
|
||||||
|
def __exit__(self, type, value, traceback):
|
||||||
|
if type is None:
|
||||||
|
self.commit()
|
||||||
|
self._states.pop()
|
||||||
|
return type == _UnexpectedError
|
||||||
|
|
||||||
|
def commit(self):
|
||||||
|
cur = self._states[-1]
|
||||||
|
prev = self._states[-2]
|
||||||
|
|
||||||
|
for key in cur.vars:
|
||||||
|
val, g = cur.vars[key]
|
||||||
|
if not g:
|
||||||
|
continue
|
||||||
|
if key in prev.vars:
|
||||||
|
prev.vars[key] = val, prev.vars[key][1]
|
||||||
|
else:
|
||||||
|
prev.vars[key] = val, True
|
||||||
|
|
||||||
|
prev.pos = cur.pos
|
||||||
|
prev.line = cur.line
|
||||||
|
prev.col = cur.col
|
||||||
|
prev.committed = True
|
||||||
|
|
||||||
|
def __nonzero__(self):
|
||||||
|
return self._states[-1].committed
|
||||||
|
|
||||||
|
__bool__ = __nonzero__
|
||||||
191
plugins/code/completers/snippets_completer/cson/writer.py
Normal file
191
plugins/code/completers/snippets_completer/cson/writer.py
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import re, json, sys
|
||||||
|
|
||||||
|
if sys.version_info[0] == 2:
|
||||||
|
def _is_num(o):
|
||||||
|
return isinstance(o, int) or isinstance(o, long) or isinstance(o, float)
|
||||||
|
def _stringify(o):
|
||||||
|
if isinstance(o, str):
|
||||||
|
return unicode(o)
|
||||||
|
if isinstance(o, unicode):
|
||||||
|
return o
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
def _is_num(o):
|
||||||
|
return isinstance(o, int) or isinstance(o, float)
|
||||||
|
def _stringify(o):
|
||||||
|
if isinstance(o, bytes):
|
||||||
|
return o.decode()
|
||||||
|
if isinstance(o, str):
|
||||||
|
return o
|
||||||
|
return None
|
||||||
|
|
||||||
|
_id_re = re.compile(r'[$a-zA-Z_][$0-9a-zA-Z_]*\Z')
|
||||||
|
|
||||||
|
class CSONEncoder:
|
||||||
|
def __init__(self, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, sort_keys=False,
|
||||||
|
indent=None, default=None):
|
||||||
|
self._skipkeys = skipkeys
|
||||||
|
self._ensure_ascii = ensure_ascii
|
||||||
|
self._allow_nan = allow_nan
|
||||||
|
self._sort_keys = sort_keys
|
||||||
|
self._indent = ' ' * (indent or 4)
|
||||||
|
self._default = default
|
||||||
|
if check_circular:
|
||||||
|
self._obj_stack = set()
|
||||||
|
else:
|
||||||
|
self._obj_stack = None
|
||||||
|
|
||||||
|
def _format_simple_val(self, o):
|
||||||
|
if o is None:
|
||||||
|
return 'null'
|
||||||
|
if isinstance(o, bool):
|
||||||
|
return 'true' if o else 'false'
|
||||||
|
if _is_num(o):
|
||||||
|
return str(o)
|
||||||
|
s = _stringify(o)
|
||||||
|
if s is not None:
|
||||||
|
return self._escape_string(s)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _escape_string(self, s):
|
||||||
|
r = json.dumps(s, ensure_ascii=self._ensure_ascii)
|
||||||
|
return u"'{}'".format(r[1:-1].replace("'", r"\'"))
|
||||||
|
|
||||||
|
def _escape_key(self, s):
|
||||||
|
if s is None or isinstance(s, bool) or _is_num(s):
|
||||||
|
s = str(s)
|
||||||
|
s = _stringify(s)
|
||||||
|
if s is None:
|
||||||
|
if self._skipkeys:
|
||||||
|
return None
|
||||||
|
raise TypeError('keys must be a string')
|
||||||
|
if not _id_re.match(s):
|
||||||
|
return self._escape_string(s)
|
||||||
|
return s
|
||||||
|
|
||||||
|
def _push_obj(self, o):
|
||||||
|
if self._obj_stack is not None:
|
||||||
|
if id(o) in self._obj_stack:
|
||||||
|
raise ValueError('Circular reference detected')
|
||||||
|
self._obj_stack.add(id(o))
|
||||||
|
|
||||||
|
def _pop_obj(self, o):
|
||||||
|
if self._obj_stack is not None:
|
||||||
|
self._obj_stack.remove(id(o))
|
||||||
|
|
||||||
|
def _encode(self, o, obj_val=False, indent='', force_flow=False):
|
||||||
|
if isinstance(o, list):
|
||||||
|
if not o:
|
||||||
|
if obj_val:
|
||||||
|
yield ' []\n'
|
||||||
|
else:
|
||||||
|
yield indent
|
||||||
|
yield '[]\n'
|
||||||
|
else:
|
||||||
|
if obj_val:
|
||||||
|
yield ' [\n'
|
||||||
|
else:
|
||||||
|
yield indent
|
||||||
|
yield '[\n'
|
||||||
|
indent = indent + self._indent
|
||||||
|
self._push_obj(o)
|
||||||
|
for v in o:
|
||||||
|
for chunk in self._encode(v, obj_val=False, indent=indent, force_flow=True):
|
||||||
|
yield chunk
|
||||||
|
self._pop_obj(o)
|
||||||
|
yield indent[:-len(self._indent)]
|
||||||
|
yield ']\n'
|
||||||
|
elif isinstance(o, dict):
|
||||||
|
items = [(self._escape_key(k), v) for k, v in o.items()]
|
||||||
|
if self._skipkeys:
|
||||||
|
items = [(k, v) for k, v in items if k is not None]
|
||||||
|
if self._sort_keys:
|
||||||
|
items.sort()
|
||||||
|
if force_flow or not items:
|
||||||
|
if not items:
|
||||||
|
if obj_val:
|
||||||
|
yield ' {}\n'
|
||||||
|
else:
|
||||||
|
yield indent
|
||||||
|
yield '{}\n'
|
||||||
|
else:
|
||||||
|
if obj_val:
|
||||||
|
yield ' {\n'
|
||||||
|
else:
|
||||||
|
yield indent
|
||||||
|
yield '{\n'
|
||||||
|
indent = indent + self._indent
|
||||||
|
self._push_obj(o)
|
||||||
|
for k, v in items:
|
||||||
|
yield indent
|
||||||
|
yield k
|
||||||
|
yield ':'
|
||||||
|
for chunk in self._encode(v, obj_val=True, indent=indent + self._indent, force_flow=False):
|
||||||
|
yield chunk
|
||||||
|
self._pop_obj(o)
|
||||||
|
yield indent[:-len(self._indent)]
|
||||||
|
yield '}\n'
|
||||||
|
else:
|
||||||
|
if obj_val:
|
||||||
|
yield '\n'
|
||||||
|
self._push_obj(o)
|
||||||
|
for k, v in items:
|
||||||
|
yield indent
|
||||||
|
yield k
|
||||||
|
yield ':'
|
||||||
|
for chunk in self._encode(v, obj_val=True, indent=indent + self._indent, force_flow=False):
|
||||||
|
yield chunk
|
||||||
|
self._pop_obj(o)
|
||||||
|
else:
|
||||||
|
v = self._format_simple_val(o)
|
||||||
|
if v is None:
|
||||||
|
self._push_obj(o)
|
||||||
|
v = self.default(o)
|
||||||
|
for chunk in self._encode(v, obj_val=obj_val, indent=indent, force_flow=force_flow):
|
||||||
|
yield chunk
|
||||||
|
self._pop_obj(o)
|
||||||
|
else:
|
||||||
|
if obj_val:
|
||||||
|
yield ' '
|
||||||
|
else:
|
||||||
|
yield indent
|
||||||
|
yield v
|
||||||
|
yield '\n'
|
||||||
|
|
||||||
|
def iterencode(self, o):
|
||||||
|
return self._encode(o)
|
||||||
|
|
||||||
|
def encode(self, o):
|
||||||
|
return ''.join(self.iterencode(o))
|
||||||
|
|
||||||
|
def default(self, o):
|
||||||
|
if self._default is None:
|
||||||
|
raise TypeError('Cannot serialize an object of type {}'.format(type(o).__name__))
|
||||||
|
return self._default(o)
|
||||||
|
|
||||||
|
def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None,
|
||||||
|
indent=None, default=None, sort_keys=False, **kw):
|
||||||
|
if indent is None and cls is None:
|
||||||
|
return json.dump(obj, fp, skipkeys=skipkeys, ensure_ascii=ensure_ascii, check_circular=check_circular,
|
||||||
|
allow_nan=allow_nan, default=default, sort_keys=sort_keys, separators=(',', ':'))
|
||||||
|
|
||||||
|
if cls is None:
|
||||||
|
cls = CSONEncoder
|
||||||
|
encoder = cls(skipkeys=skipkeys, ensure_ascii=ensure_ascii, check_circular=check_circular,
|
||||||
|
allow_nan=allow_nan, sort_keys=sort_keys, indent=indent, default=default, **kw)
|
||||||
|
|
||||||
|
for chunk in encoder.iterencode(obj):
|
||||||
|
fp.write(chunk)
|
||||||
|
|
||||||
|
def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None, indent=None,
|
||||||
|
default=None, sort_keys=False, **kw):
|
||||||
|
if indent is None and cls is None:
|
||||||
|
return json.dumps(obj, skipkeys=skipkeys, ensure_ascii=ensure_ascii, check_circular=check_circular,
|
||||||
|
allow_nan=allow_nan, default=default, sort_keys=sort_keys, separators=(',', ':'))
|
||||||
|
|
||||||
|
if cls is None:
|
||||||
|
cls = CSONEncoder
|
||||||
|
encoder = cls(skipkeys=skipkeys, ensure_ascii=ensure_ascii, check_circular=check_circular,
|
||||||
|
allow_nan=allow_nan, sort_keys=sort_keys, indent=indent, default=default, **kw)
|
||||||
|
|
||||||
|
return encoder.encode(obj)
|
||||||
7
plugins/code/completers/snippets_completer/manifest.json
Normal file
7
plugins/code/completers/snippets_completer/manifest.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"name": "Snippets Completer",
|
||||||
|
"author": "ITDominator",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"support": "",
|
||||||
|
"requests": {}
|
||||||
|
}
|
||||||
49
plugins/code/completers/snippets_completer/plugin.py
Normal file
49
plugins/code/completers/snippets_completer/plugin.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Python imports
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
import gi
|
||||||
|
gi.require_version('Gtk', '3.0')
|
||||||
|
from gi.repository import Gtk
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from libs.event_factory import Event_Factory, Code_Event_Types
|
||||||
|
|
||||||
|
from plugins.plugin_types import PluginCode
|
||||||
|
|
||||||
|
from .provider import Provider
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Plugin(PluginCode):
|
||||||
|
def __init__(self):
|
||||||
|
super(Plugin, self).__init__()
|
||||||
|
|
||||||
|
self.provider: Provider = None
|
||||||
|
|
||||||
|
|
||||||
|
def _controller_message(self, event: Code_Event_Types.CodeEvent):
|
||||||
|
...
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
self.provider = Provider()
|
||||||
|
|
||||||
|
event = Event_Factory.create_event(
|
||||||
|
"register_provider",
|
||||||
|
provider_name = "Snippets Completer",
|
||||||
|
provider = self.provider,
|
||||||
|
language_ids = []
|
||||||
|
)
|
||||||
|
self.emit_to("completion", event)
|
||||||
|
|
||||||
|
def unload(self):
|
||||||
|
event = Event_Factory.create_event(
|
||||||
|
"unregister_provider",
|
||||||
|
provider_name = "Snippets Completer"
|
||||||
|
)
|
||||||
|
self.emit_to("completion", event)
|
||||||
|
|
||||||
|
self.provider = None
|
||||||
|
del self.provider
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
...
|
||||||
68
plugins/code/completers/snippets_completer/provider.py
Normal file
68
plugins/code/completers/snippets_completer/provider.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# Python imports
|
||||||
|
import re
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
import gi
|
||||||
|
gi.require_version('GtkSource', '4')
|
||||||
|
|
||||||
|
from gi.repository import GObject
|
||||||
|
from gi.repository import GtkSource
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from .provider_response_cache import ProviderResponseCache
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Provider(GObject.GObject, GtkSource.CompletionProvider):
|
||||||
|
"""
|
||||||
|
This is a Snippits Completion Provider.
|
||||||
|
# NOTE: used information from here --> https://warroom.rsmus.com/do-that-auto-complete/
|
||||||
|
"""
|
||||||
|
__gtype_name__ = 'SnippetsCompletionProvider'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(Provider, self).__init__()
|
||||||
|
|
||||||
|
self.response_cache: ProviderResponseCache = ProviderResponseCache()
|
||||||
|
|
||||||
|
|
||||||
|
def do_get_name(self):
|
||||||
|
return 'Snippits Completion'
|
||||||
|
|
||||||
|
def do_match(self, context):
|
||||||
|
word = self.response_cache.get_word(context)
|
||||||
|
if not word or len(word) < 2: return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def do_get_priority(self):
|
||||||
|
return 2
|
||||||
|
|
||||||
|
def do_activate_proposal(self, proposal, iter_):
|
||||||
|
buffer = iter_.get_buffer()
|
||||||
|
# Note: Flag mostly intended for SourceViewsMultiInsertState
|
||||||
|
# to insure marker processes inserted text correctly.
|
||||||
|
buffer.is_processing_completion = True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def do_get_activation(self):
|
||||||
|
""" The context for when a provider will show results """
|
||||||
|
# return GtkSource.CompletionActivation.NONE
|
||||||
|
return GtkSource.CompletionActivation.USER_REQUESTED
|
||||||
|
# return GtkSource.CompletionActivation.INTERACTIVE
|
||||||
|
|
||||||
|
def do_populate(self, context):
|
||||||
|
word = self.response_cache.get_word(context)
|
||||||
|
results = self.response_cache.filter(word)
|
||||||
|
proposals = []
|
||||||
|
|
||||||
|
for entry in results:
|
||||||
|
proposals.append(
|
||||||
|
self.response_cache.create_completion_item(
|
||||||
|
entry["label"],
|
||||||
|
entry["text"],
|
||||||
|
entry["info"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
context.add_proposals(self, proposals, True)
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# Python imports
|
||||||
|
from os import path
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
import gi
|
||||||
|
gi.require_version('GtkSource', '4')
|
||||||
|
|
||||||
|
from gi.repository import GLib
|
||||||
|
from gi.repository import GtkSource
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from libs.event_factory import Code_Event_Types
|
||||||
|
|
||||||
|
from core.widgets.code.completion_providers.provider_response_cache_base import ProviderResponseCacheBase
|
||||||
|
|
||||||
|
from . import cson
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderResponseCache(ProviderResponseCacheBase):
|
||||||
|
def __init__(self):
|
||||||
|
super(ProviderResponseCache, self).__init__()
|
||||||
|
|
||||||
|
self.matchers: dict = {}
|
||||||
|
|
||||||
|
self.load_snippets()
|
||||||
|
|
||||||
|
|
||||||
|
def load_snippets(self):
|
||||||
|
fpath = path.join(path.dirname(path.realpath(__file__)), "snippets.cson")
|
||||||
|
with open(fpath, 'rb') as f:
|
||||||
|
self.snippets = cson.load(f)
|
||||||
|
for group in self.snippets:
|
||||||
|
self.snippets[group]
|
||||||
|
for entry in self.snippets[group]:
|
||||||
|
data = self.snippets[group][entry]
|
||||||
|
self.matchers[ data["prefix"] ] = {
|
||||||
|
"label": entry,
|
||||||
|
"text": data["body"],
|
||||||
|
"info": GLib.markup_escape_text( data["body"] )
|
||||||
|
}
|
||||||
|
|
||||||
|
def process_file_load(self, event: Code_Event_Types.AddedNewFileEvent):
|
||||||
|
...
|
||||||
|
|
||||||
|
def process_file_close(self, event: Code_Event_Types.RemovedFileEvent):
|
||||||
|
...
|
||||||
|
|
||||||
|
def process_file_save(self, event: Code_Event_Types.SavedFileEvent):
|
||||||
|
...
|
||||||
|
|
||||||
|
def process_file_change(self, event: Code_Event_Types.TextChangedEvent):
|
||||||
|
...
|
||||||
|
|
||||||
|
def filter(self, word: str) -> list[dict]:
|
||||||
|
response: list[dict] = []
|
||||||
|
|
||||||
|
for entry in self.matchers:
|
||||||
|
if not word in entry: continue
|
||||||
|
data = self.matchers[entry]
|
||||||
|
response.append(data)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def filter_with_context(self, context: GtkSource.CompletionContext) -> list[dict]:
|
||||||
|
response: list[dict] = []
|
||||||
|
|
||||||
|
return response
|
||||||
614
plugins/code/completers/snippets_completer/snippets.cson
Normal file
614
plugins/code/completers/snippets_completer/snippets.cson
Normal file
@@ -0,0 +1,614 @@
|
|||||||
|
# Your snippets
|
||||||
|
#
|
||||||
|
# Atom snippets allow you to enter a simple prefix in the editor and hit tab to
|
||||||
|
# expand the prefix into a larger code block with templated values.
|
||||||
|
#
|
||||||
|
# You can create a new snippet in this file by typing "snip" and then hitting
|
||||||
|
# tab.
|
||||||
|
#
|
||||||
|
# An example CoffeeScript snippet to expand log to console.log:
|
||||||
|
#
|
||||||
|
# '.source.coffee':
|
||||||
|
# 'Console log':
|
||||||
|
# 'prefix': 'log'
|
||||||
|
# 'body': 'console.log $1'
|
||||||
|
#
|
||||||
|
# Each scope (e.g. '.source.coffee' above) can only be declared once.
|
||||||
|
#
|
||||||
|
# This file uses CoffeeScript Object Notation (CSON).
|
||||||
|
# If you are unfamiliar with CSON, you can read more about it in the
|
||||||
|
# Atom Flight Manual:
|
||||||
|
# http://flight-manual.atom.io/using-atom/sections/basic-customization/#_cson
|
||||||
|
|
||||||
|
|
||||||
|
### HTML SNIPPETS ###
|
||||||
|
'.text.html.basic':
|
||||||
|
|
||||||
|
'HTML Template':
|
||||||
|
'prefix': 'html'
|
||||||
|
'body': """<!DOCTYPE html>
|
||||||
|
<html lang="en" dir="ltr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title></title>
|
||||||
|
<link rel="shortcut icon" href="fave_icon.png">
|
||||||
|
<link rel="stylesheet" href="resources/css/base.css">
|
||||||
|
<link rel="stylesheet" href="resources/css/main.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script src="resources/js/.js" charset="utf-8"></script>
|
||||||
|
<script src="resources/js/.js" charset="utf-8"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
'Canvas Tag':
|
||||||
|
'prefix': 'canvas'
|
||||||
|
'body': """<canvas id="canvas" width="800" height="600" style="border:1px solid #c3c3c3;"></canvas>"""
|
||||||
|
|
||||||
|
'Img Tag':
|
||||||
|
'prefix': 'img'
|
||||||
|
'body': """<img class="" src="" alt="" />"""
|
||||||
|
|
||||||
|
'Br Tag':
|
||||||
|
'prefix': 'br'
|
||||||
|
'body': """<br/>"""
|
||||||
|
|
||||||
|
'Hr Tag':
|
||||||
|
'prefix': 'hr'
|
||||||
|
'body': """<hr/>"""
|
||||||
|
|
||||||
|
'Server Side Events':
|
||||||
|
'prefix': 'sse'
|
||||||
|
'body': """// SSE events if supported
|
||||||
|
if(typeof(EventSource) !== "undefined") {
|
||||||
|
let source = new EventSource("resources/php/sse.php");
|
||||||
|
source.onmessage = (event) => {
|
||||||
|
if (event.data === "<yourDataStringToLookFor>") {
|
||||||
|
// code here
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.log("SSE Not Supported In Browser...");
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
'AJAX Template Function':
|
||||||
|
'prefix': 'ajax template'
|
||||||
|
'body': """const doAjax = async (actionPath, data) => {
|
||||||
|
let xhttp = new XMLHttpRequest();
|
||||||
|
|
||||||
|
xhttp.onreadystatechange = function() {
|
||||||
|
if (this.readyState === 4 && this.status === 200) {
|
||||||
|
if (this.responseText != null) { // this.responseXML if getting XML fata
|
||||||
|
handleReturnData(JSON.parse(this.responseText));
|
||||||
|
} else {
|
||||||
|
console.log("No content returned. Check the file path.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhttp.open("POST", actionPath, true);
|
||||||
|
xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
||||||
|
// Force return to be JSON NOTE: Use application/xml to force XML
|
||||||
|
xhttp.overrideMimeType('application/json');
|
||||||
|
xhttp.send(data);
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
'CSS Message Colors':
|
||||||
|
'prefix': 'css colors'
|
||||||
|
'body': """.error { color: rgb(255, 0, 0); }
|
||||||
|
.warning { color: rgb(255, 168, 0); }
|
||||||
|
.success { color: rgb(136, 204, 39); }
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
### JS SNIPPETS ###
|
||||||
|
'.source.js':
|
||||||
|
|
||||||
|
'Server Side Events':
|
||||||
|
'prefix': 'sse'
|
||||||
|
'body': """// SSE events if supported
|
||||||
|
if(typeof(EventSource) !== "undefined") {
|
||||||
|
let source = new EventSource("resources/php/sse.php");
|
||||||
|
source.onmessage = (event) => {
|
||||||
|
if (event.data === "<yourDataStringToLookFor>") {
|
||||||
|
// code here
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.log("SSE Not Supported In Browser...");
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
'AJAX Template Function':
|
||||||
|
'prefix': 'ajax template'
|
||||||
|
'body': """const doAjax = async (actionPath, data) => {
|
||||||
|
let xhttp = new XMLHttpRequest();
|
||||||
|
|
||||||
|
xhttp.onreadystatechange = function() {
|
||||||
|
if (this.readyState === 4 && this.status === 200) {
|
||||||
|
if (this.responseText != null) { // this.responseXML if getting XML fata
|
||||||
|
handleReturnData(JSON.parse(this.responseText));
|
||||||
|
} else {
|
||||||
|
console.log("No content returned. Check the file path.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhttp.open("POST", actionPath, true);
|
||||||
|
xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
||||||
|
// Force return to be JSON NOTE: Use application/xml to force XML
|
||||||
|
xhttp.overrideMimeType('application/json');
|
||||||
|
xhttp.send(data);
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
'SE6 Function':
|
||||||
|
'prefix': 'function se6'
|
||||||
|
'body': """const funcName = (arg = "") => {
|
||||||
|
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
### CSS SNIPPETS ###
|
||||||
|
'.source.css':
|
||||||
|
|
||||||
|
'CSS Message Colors':
|
||||||
|
'prefix': 'css colors'
|
||||||
|
'body': """.error { color: rgb(255, 0, 0); }
|
||||||
|
.warning { color: rgb(255, 168, 0); }
|
||||||
|
.success { color: rgb(136, 204, 39); }
|
||||||
|
"""
|
||||||
|
|
||||||
|
### PHP SNIPPETS ###
|
||||||
|
'.text.html.php':
|
||||||
|
|
||||||
|
'SSE PHP':
|
||||||
|
'prefix': 'sse php'
|
||||||
|
'body': """<?php
|
||||||
|
// Start the session
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
header('Content-Type: text/event-stream');
|
||||||
|
header('Cache-Control: no-cache');
|
||||||
|
|
||||||
|
echo "data:dataToReturn\\\\n\\\\n";
|
||||||
|
|
||||||
|
flush();
|
||||||
|
?>
|
||||||
|
"""
|
||||||
|
|
||||||
|
'PHP Template':
|
||||||
|
'prefix': 'php'
|
||||||
|
'body': """<?php
|
||||||
|
// Start the session
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
|
||||||
|
// Determin action
|
||||||
|
chdir("../../"); // Note: If in resources/php/
|
||||||
|
if (isset($_POST['yourPostID'])) {
|
||||||
|
// code here
|
||||||
|
} else {
|
||||||
|
$message = "Server: [Error] --> Illegal Access Method!";
|
||||||
|
serverMessage("error", $message);
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
"""
|
||||||
|
'HTML Template':
|
||||||
|
'prefix': 'html'
|
||||||
|
'body': """<!DOCTYPE html>
|
||||||
|
<html lang="en" dir="ltr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title></title>
|
||||||
|
<link rel="shortcut icon" href="fave_icon.png">
|
||||||
|
<link rel="stylesheet" href="resources/css/base.css">
|
||||||
|
<link rel="stylesheet" href="resources/css/main.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script src="resources/js/.js" charset="utf-8"></script>
|
||||||
|
<script src="resources/js/.js" charset="utf-8"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
### BASH SNIPPETS ###
|
||||||
|
'.source.shell':
|
||||||
|
|
||||||
|
'Bash or Shell Template':
|
||||||
|
'prefix': 'bash template'
|
||||||
|
'body': """#!/bin/bash
|
||||||
|
|
||||||
|
# . CONFIG.sh
|
||||||
|
|
||||||
|
# set -o xtrace ## To debug scripts
|
||||||
|
# set -o errexit ## To exit on error
|
||||||
|
# set -o errunset ## To exit if a variable is referenced but not set
|
||||||
|
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
echo "Working Dir: " $(pwd)
|
||||||
|
|
||||||
|
file="$1"
|
||||||
|
if [ -z "${file}" ]; then
|
||||||
|
echo "ERROR: No file argument supplied..."
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "${file}" ]]; then
|
||||||
|
echo "SUCCESS: The path and file exists!"
|
||||||
|
else
|
||||||
|
echo "ERROR: The path or file '${file}' does NOT exist..."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
main "$@";
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
'Bash or Shell Config':
|
||||||
|
'prefix': 'bash config'
|
||||||
|
'body': """#!/bin/bash
|
||||||
|
|
||||||
|
shopt -s expand_aliases
|
||||||
|
|
||||||
|
alias echo="echo -e"
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
### PYTHON SNIPPETS ###
|
||||||
|
'.source.python':
|
||||||
|
|
||||||
|
'Glade __main__ Class Template':
|
||||||
|
'prefix': 'glade __main__ class'
|
||||||
|
'body': """#!/usr/bin/python3
|
||||||
|
|
||||||
|
|
||||||
|
# Python imports
|
||||||
|
import argparse
|
||||||
|
import faulthandler
|
||||||
|
import traceback
|
||||||
|
import signal
|
||||||
|
from setproctitle import setproctitle
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
import gi
|
||||||
|
gi.require_version('Gtk', '3.0')
|
||||||
|
from gi.repository import Gtk
|
||||||
|
from gi.repository import GLib
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from app import Application
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
try:
|
||||||
|
setproctitle('<replace this>')
|
||||||
|
faulthandler.enable() # For better debug info
|
||||||
|
|
||||||
|
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, Gtk.main_quit)
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
# Add long and short arguments
|
||||||
|
parser.add_argument("--debug", "-d", default="false", help="Do extra console messaging.")
|
||||||
|
parser.add_argument("--trace-debug", "-td", default="false", help="Disable saves, ignore IPC lock, do extra console messaging.")
|
||||||
|
parser.add_argument("--no-plugins", "-np", default="false", help="Do not load plugins.")
|
||||||
|
|
||||||
|
parser.add_argument("--new-tab", "-t", default="", help="Open a file into new tab.")
|
||||||
|
parser.add_argument("--new-window", "-w", default="", help="Open a file into a new window.")
|
||||||
|
|
||||||
|
# Read arguments (If any...)
|
||||||
|
args, unknownargs = parser.parse_known_args()
|
||||||
|
|
||||||
|
main = Application(args, unknownargs)
|
||||||
|
Gtk.main()
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
quit()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
''' Set process title, get arguments, and create GTK main thread. '''
|
||||||
|
run()
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
'Glade __main__ Testing Template':
|
||||||
|
'prefix': 'glade testing class'
|
||||||
|
'body': """#!/usr/bin/python3
|
||||||
|
|
||||||
|
|
||||||
|
# Python imports
|
||||||
|
import traceback
|
||||||
|
import faulthandler
|
||||||
|
import signal
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
import gi
|
||||||
|
gi.require_version('Gtk', '3.0')
|
||||||
|
from gi.repository import Gtk
|
||||||
|
from gi.repository import GLib
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
|
||||||
|
|
||||||
|
app_name = "Gtk Quick Test"
|
||||||
|
|
||||||
|
|
||||||
|
class Application(Gtk.ApplicationWindow):
|
||||||
|
def __init__(self):
|
||||||
|
super(Application, self).__init__()
|
||||||
|
self._setup_styling()
|
||||||
|
self._setup_signals()
|
||||||
|
self._load_widgets()
|
||||||
|
|
||||||
|
self.add(Gtk.Box())
|
||||||
|
|
||||||
|
self.show_all()
|
||||||
|
|
||||||
|
|
||||||
|
def _setup_styling(self):
|
||||||
|
self.set_default_size(1670, 830)
|
||||||
|
self.set_title(f"{app_name}")
|
||||||
|
# self.set_icon_from_file( settings.get_window_icon() )
|
||||||
|
self.set_gravity(5) # 5 = CENTER
|
||||||
|
self.set_position(1) # 1 = CENTER, 4 = CENTER_ALWAYS
|
||||||
|
|
||||||
|
def _setup_signals(self):
|
||||||
|
self.connect("delete-event", Gtk.main_quit)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_widgets(self):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
try:
|
||||||
|
faulthandler.enable() # For better debug info
|
||||||
|
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, Gtk.main_quit)
|
||||||
|
|
||||||
|
main = Application()
|
||||||
|
Gtk.main()
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
quit()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
''' Set process title, get arguments, and create GTK main thread. '''
|
||||||
|
run()
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
'Glade _init_ Class Template':
|
||||||
|
'prefix': 'glade __init__ class'
|
||||||
|
'body': """# Python imports
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from utils import Settings
|
||||||
|
from signal_classes import CrossClassSignals
|
||||||
|
|
||||||
|
|
||||||
|
class Main:
|
||||||
|
def __init__(self, args):
|
||||||
|
settings = Settings()
|
||||||
|
builder = settings.returnBuilder()
|
||||||
|
|
||||||
|
# Gets the methods from the classes and sets to handler.
|
||||||
|
# Then, builder connects to any signals it needs.
|
||||||
|
classes = [CrossClassSignals(settings)]
|
||||||
|
|
||||||
|
handlers = {}
|
||||||
|
for c in classes:
|
||||||
|
methods = inspect.getmembers(c, predicate=inspect.ismethod)
|
||||||
|
handlers.update(methods)
|
||||||
|
|
||||||
|
builder.connect_signals(handlers)
|
||||||
|
window = settings.createWindow()
|
||||||
|
window.show()
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
'Class Method':
|
||||||
|
'prefix': 'def1'
|
||||||
|
'body': """
|
||||||
|
def fname(self):
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
|
||||||
|
'Gtk Class Method':
|
||||||
|
'prefix': 'def2'
|
||||||
|
'body': """
|
||||||
|
def fname(self, widget, eve):
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
'Python Glade Settings Template':
|
||||||
|
'prefix': 'glade settings class'
|
||||||
|
'body': """# Python imports
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
import gi, cairo
|
||||||
|
gi.require_version('Gtk', '3.0')
|
||||||
|
gi.require_version('Gdk', '3.0')
|
||||||
|
|
||||||
|
from gi.repository import Gtk
|
||||||
|
from gi.repository import Gdk
|
||||||
|
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
|
||||||
|
|
||||||
|
class Settings:
|
||||||
|
def __init__(self):
|
||||||
|
self.SCRIPT_PTH = os.path.dirname(os.path.realpath(__file__)) + "/"
|
||||||
|
self.builder = Gtk.Builder()
|
||||||
|
self.builder.add_from_file(self.SCRIPT_PTH + "../resources/Main_Window.glade")
|
||||||
|
|
||||||
|
# 'Filters'
|
||||||
|
self.office = ('.doc', '.docx', '.xls', '.xlsx', '.xlt', '.xltx', '.xlm',
|
||||||
|
'.ppt', 'pptx', '.pps', '.ppsx', '.odt', '.rtf')
|
||||||
|
self.vids = ('.mkv', '.avi', '.flv', '.mov', '.m4v', '.mpg', '.wmv',
|
||||||
|
'.mpeg', '.mp4', '.webm')
|
||||||
|
self.txt = ('.txt', '.text', '.sh', '.cfg', '.conf')
|
||||||
|
self.music = ('.psf', '.mp3', '.ogg' , '.flac')
|
||||||
|
self.images = ('.png', '.jpg', '.jpeg', '.gif')
|
||||||
|
self.pdf = ('.pdf')
|
||||||
|
|
||||||
|
|
||||||
|
def createWindow(self):
|
||||||
|
# Get window and connect signals
|
||||||
|
window = self.builder.get_object("Main_Window")
|
||||||
|
window.connect("delete-event", gtk.main_quit)
|
||||||
|
self.setWindowData(window, False)
|
||||||
|
return window
|
||||||
|
|
||||||
|
def setWindowData(self, window, paintable):
|
||||||
|
screen = window.get_screen()
|
||||||
|
visual = screen.get_rgba_visual()
|
||||||
|
|
||||||
|
if visual != None and screen.is_composited():
|
||||||
|
window.set_visual(visual)
|
||||||
|
|
||||||
|
# bind css file
|
||||||
|
cssProvider = gtk.CssProvider()
|
||||||
|
cssProvider.load_from_path(self.SCRIPT_PTH + '../resources/stylesheet.css')
|
||||||
|
screen = Gdk.Screen.get_default()
|
||||||
|
styleContext = Gtk.StyleContext()
|
||||||
|
styleContext.add_provider_for_screen(screen, cssProvider, gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||||
|
|
||||||
|
window.set_app_paintable(paintable)
|
||||||
|
if paintable:
|
||||||
|
window.connect("draw", self.area_draw)
|
||||||
|
|
||||||
|
def getMonitorData(self):
|
||||||
|
screen = self.builder.get_object("Main_Window").get_screen()
|
||||||
|
monitors = []
|
||||||
|
for m in range(screen.get_n_monitors()):
|
||||||
|
monitors.append(screen.get_monitor_geometry(m))
|
||||||
|
|
||||||
|
for monitor in monitors:
|
||||||
|
print(str(monitor.width) + "x" + str(monitor.height) + "+" + str(monitor.x) + "+" + str(monitor.y))
|
||||||
|
|
||||||
|
return monitors
|
||||||
|
|
||||||
|
def area_draw(self, widget, cr):
|
||||||
|
cr.set_source_rgba(0, 0, 0, 0.54)
|
||||||
|
cr.set_operator(cairo.OPERATOR_SOURCE)
|
||||||
|
cr.paint()
|
||||||
|
cr.set_operator(cairo.OPERATOR_OVER)
|
||||||
|
|
||||||
|
|
||||||
|
def returnBuilder(self): return self.builder
|
||||||
|
|
||||||
|
# Filter returns
|
||||||
|
def returnOfficeFilter(self): return self.office
|
||||||
|
def returnVidsFilter(self): return self.vids
|
||||||
|
def returnTextFilter(self): return self.txt
|
||||||
|
def returnMusicFilter(self): return self.music
|
||||||
|
def returnImagesFilter(self): return self.images
|
||||||
|
def returnPdfFilter(self): return self.pdf
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
'Python Glade CrossClassSignals Template':
|
||||||
|
'prefix': 'glade crossClassSignals class'
|
||||||
|
'body': """# Python imports
|
||||||
|
import threading
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
|
||||||
|
|
||||||
|
def threaded(fn):
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
threading.Thread(target=fn, args=args, kwargs=kwargs).start()
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
class CrossClassSignals:
|
||||||
|
def __init__(self, settings):
|
||||||
|
self.settings = settings
|
||||||
|
self.builder = self.settings.returnBuilder()
|
||||||
|
|
||||||
|
|
||||||
|
def getClipboardData(self):
|
||||||
|
proc = subprocess.Popen(['xclip','-selection', 'clipboard', '-o'], stdout=subprocess.PIPE)
|
||||||
|
retcode = proc.wait()
|
||||||
|
data = proc.stdout.read()
|
||||||
|
return data.decode("utf-8").strip()
|
||||||
|
|
||||||
|
def setClipboardData(self, data):
|
||||||
|
proc = subprocess.Popen(['xclip','-selection','clipboard'], stdin=subprocess.PIPE)
|
||||||
|
proc.stdin.write(data)
|
||||||
|
proc.stdin.close()
|
||||||
|
retcode = proc.wait()
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
'Python Glade Generic Template':
|
||||||
|
'prefix': 'glade generic class'
|
||||||
|
'body': """# Python imports
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
|
||||||
|
|
||||||
|
class GenericClass:
|
||||||
|
def __init__(self):
|
||||||
|
super(GenericClass, self).__init__()
|
||||||
|
|
||||||
|
self._setup_styling()
|
||||||
|
self._setup_signals()
|
||||||
|
self._subscribe_to_events()
|
||||||
|
self._load_widgets()
|
||||||
|
|
||||||
|
|
||||||
|
def _setup_styling(self):
|
||||||
|
...
|
||||||
|
|
||||||
|
def _setup_signals(self):
|
||||||
|
...
|
||||||
|
|
||||||
|
def _subscribe_to_events(self):
|
||||||
|
event_system.subscribe("handle_file_from_ipc", self.handle_file_from_ipc)
|
||||||
|
|
||||||
|
def _load_widgets(self):
|
||||||
|
...
|
||||||
|
|
||||||
|
"""
|
||||||
3
plugins/code/completers/words_completer/__init__.py
Normal file
3
plugins/code/completers/words_completer/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Pligin Module
|
||||||
|
"""
|
||||||
3
plugins/code/completers/words_completer/__main__.py
Normal file
3
plugins/code/completers/words_completer/__main__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Pligin Package
|
||||||
|
"""
|
||||||
68
plugins/code/completers/words_completer/default_provider.py
Normal file
68
plugins/code/completers/words_completer/default_provider.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# Python imports
|
||||||
|
import re
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
import gi
|
||||||
|
gi.require_version('Gtk', '3.0')
|
||||||
|
gi.require_version('GtkSource', '4')
|
||||||
|
|
||||||
|
from gi.repository import GObject
|
||||||
|
from gi.repository import Gtk
|
||||||
|
from gi.repository import GtkSource
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from .provider_response_cache import ProviderResponseCache
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Provider(GtkSource.CompletionWords):
|
||||||
|
"""
|
||||||
|
This is a Words Completion Provider.
|
||||||
|
# NOTE: used information from here --> https://warroom.rsmus.com/do-that-auto-complete/
|
||||||
|
"""
|
||||||
|
__gtype_name__ = 'WordsCompletionProvider'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(Provider, self).__init__()
|
||||||
|
|
||||||
|
self.response_cache: ProviderResponseCache = ProviderResponseCache()
|
||||||
|
|
||||||
|
|
||||||
|
def do_get_name(self):
|
||||||
|
return 'Words Completion'
|
||||||
|
|
||||||
|
def do_match(self, context):
|
||||||
|
# Note: If provider is in interactive activation then need to check
|
||||||
|
# view focus as otherwise non focus views start trying to grab it.
|
||||||
|
completion = context.get_property("completion")
|
||||||
|
if not completion.get_view().has_focus(): return
|
||||||
|
|
||||||
|
word = self.response_cache.get_word(context)
|
||||||
|
if not word or len(word) < 2: return False
|
||||||
|
|
||||||
|
iter = self.response_cache.get_iter_correctly(context)
|
||||||
|
iter.backward_char()
|
||||||
|
ch = iter.get_char()
|
||||||
|
# NOTE: Look to re-add or apply supprting logic to use spaces
|
||||||
|
# As is it slows down the editor in certain contexts...
|
||||||
|
# if not (ch in ('_', '.', ' ') or ch.isalnum()):
|
||||||
|
if not (ch in ('_', '.') or ch.isalnum()):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def do_get_priority(self):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def do_activate_proposal(self, proposal, iter_):
|
||||||
|
buffer = iter_.get_buffer()
|
||||||
|
# Note: Flag mostly intended for SourceViewsMultiInsertState
|
||||||
|
# to insure marker processes inserted text correctly.
|
||||||
|
buffer.is_processing_completion = True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def do_get_activation(self):
|
||||||
|
""" The context for when a provider will show results """
|
||||||
|
# return GtkSource.CompletionActivation.NONE
|
||||||
|
# return GtkSource.CompletionActivation.USER_REQUESTED
|
||||||
|
return GtkSource.CompletionActivation.INTERACTIVE
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Python imports
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from libs.event_factory import Code_Event_Types
|
||||||
|
|
||||||
|
from core.widgets.code.completion_providers.provider_response_cache_base import ProviderResponseCacheBase
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderResponseCache(ProviderResponseCacheBase):
|
||||||
|
def __init__(self):
|
||||||
|
super(ProviderResponseCache, self).__init__()
|
||||||
|
|
||||||
|
self.matchers: dict = {}
|
||||||
|
|
||||||
|
|
||||||
|
def process_file_load(self, event: Code_Event_Types.AddedNewFileEvent):
|
||||||
|
...
|
||||||
|
|
||||||
|
def process_file_close(self, event: Code_Event_Types.RemovedFileEvent):
|
||||||
|
...
|
||||||
|
|
||||||
|
def process_file_save(self, event: Code_Event_Types.SavedFileEvent):
|
||||||
|
...
|
||||||
|
|
||||||
|
def process_file_change(self, event: Code_Event_Types.TextChangedEvent):
|
||||||
|
...
|
||||||
7
plugins/code/completers/words_completer/manifest.json
Normal file
7
plugins/code/completers/words_completer/manifest.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"name": "Words Completer",
|
||||||
|
"author": "ITDominator",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"support": "",
|
||||||
|
"requests": {}
|
||||||
|
}
|
||||||
49
plugins/code/completers/words_completer/plugin.py
Normal file
49
plugins/code/completers/words_completer/plugin.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Python imports
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
import gi
|
||||||
|
gi.require_version('Gtk', '3.0')
|
||||||
|
from gi.repository import Gtk
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from libs.event_factory import Event_Factory, Code_Event_Types
|
||||||
|
|
||||||
|
from plugins.plugin_types import PluginCode
|
||||||
|
|
||||||
|
from .provider import Provider
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Plugin(PluginCode):
|
||||||
|
def __init__(self):
|
||||||
|
super(Plugin, self).__init__()
|
||||||
|
|
||||||
|
self.provider: Provider = None
|
||||||
|
|
||||||
|
|
||||||
|
def _controller_message(self, event: Code_Event_Types.CodeEvent):
|
||||||
|
...
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
self.provider = Provider()
|
||||||
|
|
||||||
|
event = Event_Factory.create_event(
|
||||||
|
"register_provider",
|
||||||
|
provider_name = "Words Completer",
|
||||||
|
provider = self.provider,
|
||||||
|
language_ids = []
|
||||||
|
)
|
||||||
|
self.emit_to("completion", event)
|
||||||
|
|
||||||
|
def unload(self):
|
||||||
|
event = Event_Factory.create_event(
|
||||||
|
"unregister_provider",
|
||||||
|
provider_name = "Words Completer"
|
||||||
|
)
|
||||||
|
self.emit_to("completion", event)
|
||||||
|
|
||||||
|
self.provider = None
|
||||||
|
del self.provider
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
...
|
||||||
86
plugins/code/completers/words_completer/provider.py
Normal file
86
plugins/code/completers/words_completer/provider.py
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# Python imports
|
||||||
|
import re
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
import gi
|
||||||
|
gi.require_version('GtkSource', '4')
|
||||||
|
|
||||||
|
from gi.repository import GObject
|
||||||
|
from gi.repository import GtkSource
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from .provider_response_cache import ProviderResponseCache
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Provider(GObject.GObject, GtkSource.CompletionProvider):
|
||||||
|
"""
|
||||||
|
This is a Words Completion Provider.
|
||||||
|
# NOTE: used information from here --> https://warroom.rsmus.com/do-that-auto-complete/
|
||||||
|
"""
|
||||||
|
__gtype_name__ = 'WordsCompletionProvider'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(Provider, self).__init__()
|
||||||
|
|
||||||
|
self.response_cache: ProviderResponseCache = ProviderResponseCache()
|
||||||
|
|
||||||
|
|
||||||
|
def do_get_name(self):
|
||||||
|
return 'Words Completion'
|
||||||
|
|
||||||
|
def do_match(self, context):
|
||||||
|
# Note: If provider is in interactive activation then need to check
|
||||||
|
# view focus as otherwise non focus views start trying to grab it.
|
||||||
|
completion = context.get_property("completion")
|
||||||
|
if not completion.get_view().has_focus(): return
|
||||||
|
|
||||||
|
word = self.response_cache.get_word(context)
|
||||||
|
if not word or len(word) < 2: return False
|
||||||
|
|
||||||
|
iter = self.response_cache.get_iter_correctly(context)
|
||||||
|
iter.backward_char()
|
||||||
|
ch = iter.get_char()
|
||||||
|
# NOTE: Look to re-add or apply supprting logic to use spaces
|
||||||
|
# As is it slows down the editor in certain contexts...
|
||||||
|
# if not (ch in ('_', '.', ' ') or ch.isalnum()):
|
||||||
|
if not (ch in ('_', '.') or ch.isalnum()):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def do_get_priority(self):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def do_activate_proposal(self, proposal, iter_):
|
||||||
|
buffer = iter_.get_buffer()
|
||||||
|
# Note: Flag mostly intended for SourceViewsMultiInsertState
|
||||||
|
# to insure marker processes inserted text correctly.
|
||||||
|
buffer.is_processing_completion = True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def do_get_activation(self):
|
||||||
|
""" The context for when a provider will show results """
|
||||||
|
# return GtkSource.CompletionActivation.NONE
|
||||||
|
# return GtkSource.CompletionActivation.USER_REQUESTED
|
||||||
|
return GtkSource.CompletionActivation.INTERACTIVE
|
||||||
|
|
||||||
|
def do_populate(self, context):
|
||||||
|
word = self.response_cache.get_word(context)
|
||||||
|
results = self.response_cache.filter_with_context(context)
|
||||||
|
# results = self.response_cache.filter(word)
|
||||||
|
|
||||||
|
# if not results:
|
||||||
|
# results = self.response_cache.filter_with_context(context)
|
||||||
|
|
||||||
|
proposals = []
|
||||||
|
for entry in results:
|
||||||
|
proposals.append(
|
||||||
|
self.response_cache.create_completion_item(
|
||||||
|
entry["label"],
|
||||||
|
entry["text"],
|
||||||
|
entry["info"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
context.add_proposals(self, proposals, True)
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
# Python imports
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
import gi
|
||||||
|
gi.require_version('GtkSource', '4')
|
||||||
|
|
||||||
|
from gi.repository import GLib
|
||||||
|
from gi.repository import GtkSource
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from libs.event_factory import Code_Event_Types
|
||||||
|
|
||||||
|
from core.widgets.code.completion_providers.provider_response_cache_base import ProviderResponseCacheBase
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderResponseCache(ProviderResponseCacheBase):
|
||||||
|
def __init__(self):
|
||||||
|
super(ProviderResponseCache, self).__init__()
|
||||||
|
|
||||||
|
self.matchers: dict = {}
|
||||||
|
self._temp_timeout_id: int = None
|
||||||
|
|
||||||
|
|
||||||
|
def process_file_load(self, event: Code_Event_Types.AddedNewFileEvent):
|
||||||
|
buffer = event.file.buffer
|
||||||
|
with ThreadPoolExecutor(max_workers = 1) as executor:
|
||||||
|
executor.submit(self._handle_change, buffer)
|
||||||
|
|
||||||
|
def process_file_close(self, event: Code_Event_Types.RemovedFileEvent):
|
||||||
|
self.matchers[event.file.buffer] = set()
|
||||||
|
del self.matchers[event.file.buffer]
|
||||||
|
|
||||||
|
def process_file_save(self, event: Code_Event_Types.SavedFileEvent):
|
||||||
|
...
|
||||||
|
|
||||||
|
def process_file_change(self, event: Code_Event_Types.TextChangedEvent):
|
||||||
|
buffer = event.file.buffer
|
||||||
|
self._clear_temp_delay()
|
||||||
|
self._set_temp_delay(buffer)
|
||||||
|
|
||||||
|
def _clear_temp_delay(self):
|
||||||
|
if self._temp_timeout_id:
|
||||||
|
GLib.source_remove(self._temp_timeout_id)
|
||||||
|
|
||||||
|
def _set_temp_delay(self, buffer):
|
||||||
|
def run_refresh_update(buffer):
|
||||||
|
with ThreadPoolExecutor(max_workers = 1) as executor:
|
||||||
|
executor.submit(self._handle_change, buffer)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
self._temp_timeout_id = GLib.timeout_add(1500, run_refresh_update, buffer)
|
||||||
|
|
||||||
|
def _handle_change(self, buffer):
|
||||||
|
start_itr = buffer.get_start_iter()
|
||||||
|
end_itr = buffer.get_end_iter()
|
||||||
|
data = buffer.get_text(start_itr, end_itr, False)
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
GLib.idle_add(self.load_empty_set, buffer)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not buffer in self.matchers:
|
||||||
|
GLib.idle_add(self.load_as_new_set, buffer, data)
|
||||||
|
return
|
||||||
|
|
||||||
|
new_words = self.get_all_words(data)
|
||||||
|
GLib.idle_add(self.load_into_set, buffer, new_words)
|
||||||
|
|
||||||
|
def filter(self, word: str) -> list[dict]:
|
||||||
|
response: list[dict] = []
|
||||||
|
|
||||||
|
for entry in self.matchers:
|
||||||
|
if not word in entry: continue
|
||||||
|
data = self.matchers[entry]
|
||||||
|
response.append(data)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def filter_with_context(self, context: GtkSource.CompletionContext) -> list[dict]:
|
||||||
|
buffer = self.get_iter_correctly(context).get_buffer()
|
||||||
|
word = self.get_word(context).rstrip()
|
||||||
|
|
||||||
|
if not buffer in self.matchers:
|
||||||
|
self.matchers[buffer] = set()
|
||||||
|
|
||||||
|
response: list[dict] = []
|
||||||
|
for entry in self.matchers[buffer]:
|
||||||
|
if not entry.rstrip().startswith(word): continue
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"label": entry,
|
||||||
|
"text": entry,
|
||||||
|
"info": ""
|
||||||
|
}
|
||||||
|
|
||||||
|
response.append(data)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def load_empty_set(self, buffer):
|
||||||
|
self.matchers[buffer] = set()
|
||||||
|
|
||||||
|
def load_into_set(self, buffer, new_words):
|
||||||
|
# self.matchers[buffer].update(new_words)
|
||||||
|
self.matchers[buffer] = new_words
|
||||||
|
|
||||||
|
def load_as_new_set(self, buffer, data):
|
||||||
|
self.matchers[buffer] = self.get_all_words(data)
|
||||||
|
|
||||||
|
def get_all_words(self, data: str):
|
||||||
|
words = set()
|
||||||
|
|
||||||
|
def is_word_char(c):
|
||||||
|
return c.isalnum() or c == '_'
|
||||||
|
|
||||||
|
size = len(data)
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
while i < size:
|
||||||
|
# Skip non-word characters
|
||||||
|
while i < size and not is_word_char(data[i]):
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
start = i
|
||||||
|
# Consume word characters
|
||||||
|
while i < size and is_word_char(data[i]):
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
word = data[start:i]
|
||||||
|
if not word: continue
|
||||||
|
words.add(word)
|
||||||
|
|
||||||
|
return words
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Plugin Module
|
||||||
|
"""
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Plugin Package
|
||||||
|
"""
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"name": "Extend Source View Menu",
|
||||||
|
"author": "ITDominator",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"support": "",
|
||||||
|
"requests": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Python imports
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from libs.event_factory import Event_Factory, Code_Event_Types
|
||||||
|
|
||||||
|
from plugins.plugin_types import PluginCode
|
||||||
|
|
||||||
|
from .source_view_menu import extend_source_view_menu
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Plugin(PluginCode):
|
||||||
|
def __init__(self):
|
||||||
|
super(Plugin, self).__init__()
|
||||||
|
|
||||||
|
def _controller_message(self, event: Code_Event_Types.CodeEvent):
|
||||||
|
if isinstance(event, Code_Event_Types.PopulateSourceViewPopupEvent):
|
||||||
|
extend_source_view_menu(event.buffer, event.menu)
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
...
|
||||||
|
|
||||||
|
def unload(self):
|
||||||
|
...
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
...
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# Python imports
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version('Gtk', '3.0')
|
||||||
|
|
||||||
|
from gi.repository import Gtk
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def on_case_handle(menuitem, buffer, action):
|
||||||
|
start_itr, \
|
||||||
|
end_itr = buffer.get_selection_bounds()
|
||||||
|
data = buffer.get_text(start_itr, end_itr, False)
|
||||||
|
text = data
|
||||||
|
|
||||||
|
if action == "on_all_upper":
|
||||||
|
text = data.upper()
|
||||||
|
elif action == "on_all_lower":
|
||||||
|
text = data.lower()
|
||||||
|
elif action == "on_invert":
|
||||||
|
text = data.swapcase()
|
||||||
|
elif action == "on_title":
|
||||||
|
text = data.title()
|
||||||
|
elif action == "on_title_strip":
|
||||||
|
text = data.title().replace("-", "").replace("_", "").replace(" ", "")
|
||||||
|
|
||||||
|
buffer.begin_user_action()
|
||||||
|
buffer.delete(start_itr, end_itr)
|
||||||
|
buffer.insert(start_itr, text)
|
||||||
|
buffer.end_user_action()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def extend_source_view_menu(buffer, menu):
|
||||||
|
if not buffer.get_selection_bounds(): return
|
||||||
|
|
||||||
|
for child in menu.get_children():
|
||||||
|
if not child.get_label() == "C_hange Case": continue
|
||||||
|
menu.remove(child)
|
||||||
|
|
||||||
|
change_case_item = Gtk.MenuItem(label = "Change Case")
|
||||||
|
|
||||||
|
case_menu = Gtk.Menu()
|
||||||
|
au_case_item = Gtk.MenuItem(label = "All Upper Case")
|
||||||
|
al_case_item = Gtk.MenuItem(label = "All Lower Case")
|
||||||
|
inver_case_item = Gtk.MenuItem(label = "Invert Case")
|
||||||
|
title_case_item = Gtk.MenuItem(label = "Title Case")
|
||||||
|
title_strip_case_item = Gtk.MenuItem(label = "Title Strip Case")
|
||||||
|
|
||||||
|
au_case_item.connect("activate", on_case_handle, buffer, "on_all_upper")
|
||||||
|
al_case_item.connect("activate", on_case_handle, buffer, "on_all_lower")
|
||||||
|
inver_case_item.connect("activate", on_case_handle, buffer, "on_invert")
|
||||||
|
title_case_item.connect("activate", on_case_handle, buffer, "on_title")
|
||||||
|
title_strip_case_item.connect("activate", on_case_handle, buffer, "on_title_strip")
|
||||||
|
|
||||||
|
case_menu.append(au_case_item)
|
||||||
|
case_menu.append(al_case_item)
|
||||||
|
case_menu.append(inver_case_item)
|
||||||
|
case_menu.append(title_case_item)
|
||||||
|
case_menu.append(title_strip_case_item)
|
||||||
|
change_case_item.set_submenu(case_menu)
|
||||||
|
|
||||||
|
menu.append(change_case_item)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Pligin Module
|
||||||
|
"""
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Pligin Package
|
||||||
|
"""
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"name": "File State Watcher",
|
||||||
|
"author": "ITDominator",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"support": "",
|
||||||
|
"requests": {}
|
||||||
|
}
|
||||||
43
plugins/code/event-watchers/file_state_watcher/plugin.py
Normal file
43
plugins/code/event-watchers/file_state_watcher/plugin.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Python imports
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from libs.event_factory import Event_Factory, Code_Event_Types
|
||||||
|
|
||||||
|
from plugins.plugin_types import PluginCode
|
||||||
|
|
||||||
|
from .watcher_checks import *
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Plugin(PluginCode):
|
||||||
|
def __init__(self):
|
||||||
|
super(Plugin, self).__init__()
|
||||||
|
|
||||||
|
|
||||||
|
def _controller_message(self, event: Code_Event_Types.CodeEvent):
|
||||||
|
if not isinstance(event, Code_Event_Types.FocusedViewEvent): return
|
||||||
|
event = Event_Factory.create_event(
|
||||||
|
"get_file", buffer = event.view.get_buffer()
|
||||||
|
)
|
||||||
|
self.emit_to("files", event)
|
||||||
|
|
||||||
|
file = event.response
|
||||||
|
if file.ftype == "buffer": return
|
||||||
|
|
||||||
|
file.check_file_on_disk()
|
||||||
|
|
||||||
|
if file.is_deleted():
|
||||||
|
file_is_deleted(file, self.emit)
|
||||||
|
elif file.is_externally_modified():
|
||||||
|
file_is_externally_modified(file, self.emit)
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
...
|
||||||
|
|
||||||
|
def unload(self):
|
||||||
|
...
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
...
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Python imports
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
import gi
|
||||||
|
gi.require_version('Gtk', '3.0')
|
||||||
|
from gi.repository import Gtk
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from libs.event_factory import Event_Factory, Code_Event_Types
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def ask_yes_no(message):
|
||||||
|
dialog = Gtk.MessageDialog(
|
||||||
|
parent = None,
|
||||||
|
flags = 0,
|
||||||
|
message_type = Gtk.MessageType.QUESTION,
|
||||||
|
buttons = Gtk.ButtonsType.YES_NO,
|
||||||
|
text = message,
|
||||||
|
)
|
||||||
|
dialog.set_title("Confirm")
|
||||||
|
|
||||||
|
response = dialog.run()
|
||||||
|
dialog.destroy()
|
||||||
|
|
||||||
|
return response == Gtk.ResponseType.YES
|
||||||
|
|
||||||
|
|
||||||
|
def file_is_deleted(file, emit):
|
||||||
|
file.was_deleted = True
|
||||||
|
event = Event_Factory.create_event(
|
||||||
|
"file_externally_deleted",
|
||||||
|
file = file,
|
||||||
|
buffer = file.buffer
|
||||||
|
)
|
||||||
|
emit(event)
|
||||||
|
|
||||||
|
|
||||||
|
def file_is_externally_modified(file, emit):
|
||||||
|
event = Event_Factory.create_event(
|
||||||
|
"file_externally_modified",
|
||||||
|
file = file,
|
||||||
|
buffer = file.buffer
|
||||||
|
)
|
||||||
|
emit(event)
|
||||||
|
|
||||||
|
if not file.buffer.get_modified():
|
||||||
|
file.reload()
|
||||||
|
return
|
||||||
|
|
||||||
|
result = ask_yes_no("File has been externally modified. Reload?")
|
||||||
|
if not result: return
|
||||||
|
|
||||||
|
file.reload()
|
||||||
3
plugins/code/event-watchers/prettify_json/__init__.py
Normal file
3
plugins/code/event-watchers/prettify_json/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Pligin Module
|
||||||
|
"""
|
||||||
3
plugins/code/event-watchers/prettify_json/__main__.py
Normal file
3
plugins/code/event-watchers/prettify_json/__main__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Pligin Package
|
||||||
|
"""
|
||||||
7
plugins/code/event-watchers/prettify_json/manifest.json
Normal file
7
plugins/code/event-watchers/prettify_json/manifest.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"name": "Prettify JSON",
|
||||||
|
"author": "ITDominator",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"support": "",
|
||||||
|
"requests": {}
|
||||||
|
}
|
||||||
39
plugins/code/event-watchers/prettify_json/plugin.py
Normal file
39
plugins/code/event-watchers/prettify_json/plugin.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Python imports
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version('Gtk', '3.0')
|
||||||
|
|
||||||
|
from gi.repository import Gtk
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from libs.event_factory import Event_Factory, Code_Event_Types
|
||||||
|
|
||||||
|
from plugins.plugin_types import PluginCode
|
||||||
|
|
||||||
|
from .prettify_json import add_prettify_json
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Plugin(PluginCode):
|
||||||
|
def __init__(self):
|
||||||
|
super(Plugin, self).__init__()
|
||||||
|
|
||||||
|
|
||||||
|
def _controller_message(self, event: Code_Event_Types.CodeEvent):
|
||||||
|
if isinstance(event, Code_Event_Types.PopulateSourceViewPopupEvent):
|
||||||
|
language = event.buffer.get_language()
|
||||||
|
if not language: return
|
||||||
|
|
||||||
|
if "json" == language.get_id():
|
||||||
|
add_prettify_json(event.buffer, event.menu)
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
...
|
||||||
|
|
||||||
|
def unload(self):
|
||||||
|
...
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
...
|
||||||
29
plugins/code/event-watchers/prettify_json/prettify_json.py
Normal file
29
plugins/code/event-watchers/prettify_json/prettify_json.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Python imports
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version('Gtk', '3.0')
|
||||||
|
|
||||||
|
from gi.repository import Gtk
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def add_prettify_json(buffer, menu):
|
||||||
|
def on_prettify_json(menuitem, buffer):
|
||||||
|
start_itr, \
|
||||||
|
end_itr = buffer.get_start_iter(), buffer.get_end_iter()
|
||||||
|
data = buffer.get_text(start_itr, end_itr, False)
|
||||||
|
text = json.dumps(json.loads(data), separators = (',', ':'), indent = 4)
|
||||||
|
|
||||||
|
buffer.begin_user_action()
|
||||||
|
buffer.delete(start_itr, end_itr)
|
||||||
|
buffer.insert(start_itr, text)
|
||||||
|
buffer.end_user_action()
|
||||||
|
|
||||||
|
item = Gtk.MenuItem(label = "Prettify JSON")
|
||||||
|
item.connect("activate", on_prettify_json, buffer)
|
||||||
|
menu.append(item)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Pligin Module
|
||||||
|
"""
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Pligin Package
|
||||||
|
"""
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
{
|
||||||
|
"info": "https://download.eclipse.org/jdtls/",
|
||||||
|
"info-init-options": "https://github.com/eclipse-jdtls/eclipse.jdt.ls/wiki/Running-the-JAVA-LS-server-from-the-command-line",
|
||||||
|
"info-import-build": "https://www.javahotchocolate.com/tutorials/build-path.html",
|
||||||
|
"info-external-class-paths": "https://github.com/eclipse-jdtls/eclipse.jdt.ls/issues/3291",
|
||||||
|
"link": "https://download.eclipse.org/jdtls/milestones/?d",
|
||||||
|
"command": "lsp-ws-proxy --listen 4114 -- jdtls",
|
||||||
|
"alt-command": "lsp-ws-proxy -- jdtls",
|
||||||
|
"alt-command2": "java-language-server",
|
||||||
|
"socket": "ws://127.0.0.1:9999/java",
|
||||||
|
"socket-two": "ws://127.0.0.1:9999/?name=jdtls",
|
||||||
|
"alt-socket": "ws://127.0.0.1:9999/?name=java-language-server",
|
||||||
|
"initialization-options": {
|
||||||
|
"bundles": [
|
||||||
|
"intellicode-core.jar"
|
||||||
|
],
|
||||||
|
"workspaceFolders": [
|
||||||
|
"file://{workspace.folder}"
|
||||||
|
],
|
||||||
|
"extendedClientCapabilities": {
|
||||||
|
"classFileContentsSupport": true,
|
||||||
|
"executeClientCommandSupport": false
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"java": {
|
||||||
|
"autobuild": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"jdt": {
|
||||||
|
"ls": {
|
||||||
|
"javac": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"java": {
|
||||||
|
"home": "{user.home}/Portable_Apps/sdks/javasdk/jdk-22.0.2"
|
||||||
|
},
|
||||||
|
"lombokSupport": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"protobufSupport":{
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"androidSupport": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"updateBuildConfiguration": "automatic",
|
||||||
|
"maven": {
|
||||||
|
"userSettings": "{user.home}/.config/jdtls/settings.xml",
|
||||||
|
"globalSettings": "{user.home}/.config/jdtls/settings.xml"
|
||||||
|
},
|
||||||
|
"runtimes": [
|
||||||
|
{
|
||||||
|
"name": "JavaSE-17",
|
||||||
|
"path": "/usr/lib/jvm/java-17-openjdk",
|
||||||
|
"javadoc": "https://docs.oracle.com/en/java/javase/17/docs/api/",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "JavaSE-22",
|
||||||
|
"path": "{user.home}/Portable_Apps/sdks/javasdk/jdk-22.0.2",
|
||||||
|
"javadoc": "https://docs.oracle.com/en/java/javase/22/docs/api/",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"classPath": [
|
||||||
|
"{user.home}/.config/jdtls/m2/repository/**/*-sources.jar",
|
||||||
|
"lib/**/*-sources.jar"
|
||||||
|
],
|
||||||
|
"docPath": [
|
||||||
|
"{user.home}/.config/jdtls/m2/repository/**/*-javadoc.jar",
|
||||||
|
"lib/**/*-javadoc.jar"
|
||||||
|
],
|
||||||
|
"project": {
|
||||||
|
"encoding": "ignore",
|
||||||
|
"outputPath": "bin",
|
||||||
|
"referencedLibraries": [
|
||||||
|
"{user.home}/.config/jdtls/m2/repository/**/*.jar",
|
||||||
|
"lib/**/*.jar"
|
||||||
|
],
|
||||||
|
"importOnFirstTimeStartup": "automatic",
|
||||||
|
"importHint": true,
|
||||||
|
"resourceFilters": [
|
||||||
|
"node_modules",
|
||||||
|
"\\.git"
|
||||||
|
],
|
||||||
|
"sourcePaths": [
|
||||||
|
"src",
|
||||||
|
"{user.home}/.config/jdtls/m2/repository/**/*.jar"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sources": {
|
||||||
|
"organizeImports": {
|
||||||
|
"starThreshold": 99,
|
||||||
|
"staticStarThreshold": 99
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"imports": {
|
||||||
|
"gradle": {
|
||||||
|
"wrapper": {
|
||||||
|
"checksums": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"import": {
|
||||||
|
"maven": {
|
||||||
|
"enabled": true,
|
||||||
|
"offline": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"disableTestClasspathFlag": false
|
||||||
|
},
|
||||||
|
"gradle": {
|
||||||
|
"enabled": false,
|
||||||
|
"wrapper": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"version": "",
|
||||||
|
"home": "abs(static/gradle-7.3.3)",
|
||||||
|
"java": {
|
||||||
|
"home": "abs(static/launch_jres/17.0.6-linux-x86_64)"
|
||||||
|
},
|
||||||
|
"offline": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"arguments": [],
|
||||||
|
"jvmArguments": [],
|
||||||
|
"user": {
|
||||||
|
"home": ""
|
||||||
|
},
|
||||||
|
"annotationProcessing": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclusions": [
|
||||||
|
"**/node_modules/**",
|
||||||
|
"**/.metadata/**",
|
||||||
|
"**/archetype-resources/**",
|
||||||
|
"**/META-INF/maven/**"
|
||||||
|
],
|
||||||
|
"generatesMetadataFilesAtProjectRoot": false
|
||||||
|
},
|
||||||
|
"maven": {
|
||||||
|
"downloadSources": true,
|
||||||
|
"updateSnapshots": true
|
||||||
|
},
|
||||||
|
"silentNotification": true,
|
||||||
|
"contentProvider": {
|
||||||
|
"preferred": "fernflower"
|
||||||
|
},
|
||||||
|
"signatureHelp": {
|
||||||
|
"enabled": true,
|
||||||
|
"description": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"completion": {
|
||||||
|
"enabled": true,
|
||||||
|
"engine": "ecj",
|
||||||
|
"matchCase": "firstletter",
|
||||||
|
"maxResults": 25,
|
||||||
|
"guessMethodArguments": true,
|
||||||
|
"lazyResolveTextEdit": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"postfix": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"favoriteStaticMembers": [
|
||||||
|
"org.junit.Assert.*",
|
||||||
|
"org.junit.Assume.*",
|
||||||
|
"org.junit.jupiter.api.Assertions.*",
|
||||||
|
"org.junit.jupiter.api.Assumptions.*",
|
||||||
|
"org.junit.jupiter.api.DynamicContainer.*",
|
||||||
|
"org.junit.jupiter.api.DynamicTest.*"
|
||||||
|
],
|
||||||
|
"importOrder": [
|
||||||
|
"#",
|
||||||
|
"java",
|
||||||
|
"javax",
|
||||||
|
"org",
|
||||||
|
"com"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"references": {
|
||||||
|
"includeAccessors": true,
|
||||||
|
"includeDecompiledSources": true
|
||||||
|
},
|
||||||
|
"codeGeneration": {
|
||||||
|
"toString": {
|
||||||
|
"template": "${object.className}{${member.name()}=${member.value}, ${otherMembers}}"
|
||||||
|
},
|
||||||
|
"insertionLocation": "afterCursor",
|
||||||
|
"useBlocks": true
|
||||||
|
},
|
||||||
|
"implementationsCodeLens": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"referencesCodeLens": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"progressReports": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"saveActions": {
|
||||||
|
"organizeImports": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "Java LSP Client",
|
||||||
|
"author": "ITDominator",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"support": "",
|
||||||
|
"autoload": false,
|
||||||
|
"requests": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# Python imports
|
||||||
|
from os import path
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
import gi
|
||||||
|
|
||||||
|
from gi.repository import GLib
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from libs.event_factory import Event_Factory, Code_Event_Types
|
||||||
|
|
||||||
|
from plugins.plugin_types import PluginCode
|
||||||
|
|
||||||
|
from .response_handler import JavaHandler
|
||||||
|
|
||||||
|
|
||||||
|
class Plugin(PluginCode):
|
||||||
|
def __init__(self):
|
||||||
|
super(Plugin, self).__init__()
|
||||||
|
|
||||||
|
|
||||||
|
def _controller_message(self, event: Code_Event_Types.CodeEvent):
|
||||||
|
...
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
dirPth = path.dirname( path.realpath(__file__) )
|
||||||
|
with open(f"{dirPth}/config/lsp-server-config.json", "r") as f:
|
||||||
|
config = f.read()
|
||||||
|
event = Event_Factory.create_event("register_lsp_client",
|
||||||
|
lang_id = "java",
|
||||||
|
lang_config = config,
|
||||||
|
handler = JavaHandler
|
||||||
|
)
|
||||||
|
self.emit_to("lsp_manager", event)
|
||||||
|
|
||||||
|
def unload(self):
|
||||||
|
event = Event_Factory.create_event("unregister_lsp_client",
|
||||||
|
lang_id = "java"
|
||||||
|
)
|
||||||
|
self.emit_to("lsp_manager", event)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
...
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
from .java import JavaHandler
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Python imports
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
import gi
|
||||||
|
gi.require_version('GtkSource', '4')
|
||||||
|
|
||||||
|
from gi.repository import GtkSource
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from libs.event_factory import Event_Factory, Code_Event_Types
|
||||||
|
|
||||||
|
from lsp_manager.response_handlers.default import DefaultHandler
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class JavaHandler(DefaultHandler):
|
||||||
|
"""Java-specific: overrides definition, handles classFileContents."""
|
||||||
|
|
||||||
|
def handle(self, method: str, response, controller):
|
||||||
|
match method:
|
||||||
|
case "textDocument/definition":
|
||||||
|
self._handle_definition(response, controller)
|
||||||
|
case "java/classFileContents":
|
||||||
|
self._handle_class_file_contents(response)
|
||||||
|
case _:
|
||||||
|
super().handle(method, response, controller)
|
||||||
|
|
||||||
|
def _handle_definition(self, response, controller):
|
||||||
|
if not response: return
|
||||||
|
|
||||||
|
uri = response[0]["uri"]
|
||||||
|
if "jdt://" in uri:
|
||||||
|
controller._lsp_java_class_file_contents(uri)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._prompt_goto_request(uri, response[0]["range"])
|
||||||
|
|
||||||
|
def _handle_class_file_contents(self, text: str):
|
||||||
|
event = Event_Factory.create_event("get_active_view")
|
||||||
|
self.emit_to("source_views", event)
|
||||||
|
|
||||||
|
view = event.response
|
||||||
|
file = view.command.exec("new_file")
|
||||||
|
buffer = view.get_buffer()
|
||||||
|
itr = buffer.get_iter_at_mark(buffer.get_insert())
|
||||||
|
lm = GtkSource.LanguageManager.get_default()
|
||||||
|
language = lm.get_language("java")
|
||||||
|
file.ftype = "java"
|
||||||
|
|
||||||
|
buffer.set_language(language)
|
||||||
|
buffer.insert(itr, text, -1)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Pligin Module
|
||||||
|
"""
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Pligin Package
|
||||||
|
"""
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
LSP Clients Module
|
||||||
|
"""
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# Python imports
|
||||||
|
import threading
|
||||||
|
from os import path
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
import gi
|
||||||
|
from gi.repository import GLib
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from ..dto.code.lsp.lsp_messages import get_message_str
|
||||||
|
from ..dto.code.lsp.lsp_message_structs import \
|
||||||
|
LSPResponseTypes, ClientRequest, ClientNotification
|
||||||
|
from .lsp_client_websocket import LSPClientWebsocket
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class LSPClient(LSPClientWebsocket):
|
||||||
|
def __init__(self):
|
||||||
|
super(LSPClient, self).__init__()
|
||||||
|
|
||||||
|
# https://github.com/microsoft/multilspy/tree/main/src/multilspy/language_servers
|
||||||
|
# initialize-params-slim.json was created off of jedi_language_server one
|
||||||
|
# self._init_params = settings_manager.get_lsp_init_data()
|
||||||
|
|
||||||
|
self._language: str = ""
|
||||||
|
self._init_params: dict = {}
|
||||||
|
self._event_history: dict[int, str] = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
_USER_HOME = path.expanduser('~')
|
||||||
|
_SCRIPT_PTH = path.dirname( path.realpath(__file__) )
|
||||||
|
_LSP_INIT_CONFIG = f"{_SCRIPT_PTH}/../configs/initialize-params-slim.json"
|
||||||
|
|
||||||
|
with open(_LSP_INIT_CONFIG) as file:
|
||||||
|
data = file.read().replace("{user.home}", _USER_HOME)
|
||||||
|
self._init_params = json.loads(data)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error( f"LSP Controller: {_LSP_INIT_CONFIG}\n\t\t{repr(e)}" )
|
||||||
|
|
||||||
|
self._message_id: int = -1
|
||||||
|
self._socket = None
|
||||||
|
self.read_lock = threading.Lock()
|
||||||
|
self.write_lock = threading.Lock()
|
||||||
|
|
||||||
|
def set_language(self, language: str):
|
||||||
|
self._language = language
|
||||||
|
|
||||||
|
def set_socket(self, socket: str):
|
||||||
|
self._socket = socket
|
||||||
|
|
||||||
|
def unset_socket(self):
|
||||||
|
self._socket = None
|
||||||
|
|
||||||
|
def send_notification(self, method: str, params: dict = {}):
|
||||||
|
self._send_message( ClientNotification(method, params) )
|
||||||
|
|
||||||
|
def send_request(self, method: str, params: dict = {}):
|
||||||
|
self._message_id += 1
|
||||||
|
self._event_history[self._message_id] = method
|
||||||
|
self._send_message( ClientRequest(self._message_id, method, params) )
|
||||||
|
|
||||||
|
def get_event_by_id(self, message_id: int) -> str:
|
||||||
|
if not message_id in self._event_history: return
|
||||||
|
return self._event_history[message_id]
|
||||||
|
|
||||||
|
def handle_lsp_response(self, lsp_response: LSPResponseTypes):
|
||||||
|
raise NotImplementedError
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Python imports
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from ..dto.code.lsp.lsp_message_structs import ClientRequest, ClientNotification
|
||||||
|
|
||||||
|
from .lsp_client_events import LSPClientEvents
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class LSPClientBase(LSPClientEvents):
|
||||||
|
def _send_message(self, data: ClientRequest or ClientNotification):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def start_client(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def stop_client(self):
|
||||||
|
raise NotImplementedError
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
# Python imports
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from ..dto.code.lsp.lsp_messages import get_message_obj
|
||||||
|
from ..dto.code.lsp.lsp_messages import didopen_notification
|
||||||
|
from ..dto.code.lsp.lsp_messages import didsave_notification
|
||||||
|
from ..dto.code.lsp.lsp_messages import didclose_notification
|
||||||
|
from ..dto.code.lsp.lsp_messages import didchange_notification
|
||||||
|
from ..dto.code.lsp.lsp_messages import completion_request
|
||||||
|
from ..dto.code.lsp.lsp_messages import definition_request
|
||||||
|
from ..dto.code.lsp.lsp_messages import references_request
|
||||||
|
from ..dto.code.lsp.lsp_messages import symbols_request
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class LSPClientEvents:
|
||||||
|
def send_initialize_message(self, init_ops: dict, workspace_file: str, workspace_uri: str):
|
||||||
|
folder_name = os.path.basename(workspace_file)
|
||||||
|
|
||||||
|
self._init_params["processId"] = None
|
||||||
|
self._init_params["rootPath"] = workspace_file
|
||||||
|
self._init_params["rootUri"] = workspace_uri
|
||||||
|
self._init_params["workspaceFolders"] = [
|
||||||
|
{
|
||||||
|
"name": folder_name,
|
||||||
|
"uri": workspace_uri
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
self._init_params["initializationOptions"] = init_ops
|
||||||
|
self.send_request("initialize", self._init_params)
|
||||||
|
|
||||||
|
def send_initialized_message(self):
|
||||||
|
self.send_notification("initialized")
|
||||||
|
|
||||||
|
def _lsp_did_open(self, data: dict):
|
||||||
|
method = "textDocument/didOpen"
|
||||||
|
params = didopen_notification["params"]
|
||||||
|
|
||||||
|
params["textDocument"]["uri"] = data["uri"]
|
||||||
|
params["textDocument"]["languageId"] = data["language_id"]
|
||||||
|
params["textDocument"]["text"] = data["text"]
|
||||||
|
|
||||||
|
self.send_notification( method, params )
|
||||||
|
|
||||||
|
def _lsp_did_save(self, data: dict):
|
||||||
|
method = "textDocument/didSave"
|
||||||
|
params = didsave_notification["params"]
|
||||||
|
|
||||||
|
params["textDocument"]["uri"] = data["uri"]
|
||||||
|
params["text"] = data["text"]
|
||||||
|
|
||||||
|
self.send_notification( method, params )
|
||||||
|
|
||||||
|
def _lsp_did_close(self, data: dict):
|
||||||
|
method = "textDocument/didClose"
|
||||||
|
params = didclose_notification["params"]
|
||||||
|
|
||||||
|
params["textDocument"]["uri"] = data["uri"]
|
||||||
|
|
||||||
|
self.send_notification( method, params )
|
||||||
|
|
||||||
|
def _lsp_did_change(self, data: dict):
|
||||||
|
method = "textDocument/didChange"
|
||||||
|
params = didchange_notification["params"]
|
||||||
|
|
||||||
|
params["textDocument"]["uri"] = data["uri"]
|
||||||
|
params["textDocument"]["languageId"] = data["language_id"]
|
||||||
|
params["textDocument"]["version"] = data["version"]
|
||||||
|
|
||||||
|
contentChanges = params["contentChanges"][0]
|
||||||
|
contentChanges["text"] = data["text"]
|
||||||
|
|
||||||
|
self.send_notification( method, params )
|
||||||
|
|
||||||
|
# def _lsp_did_change(self, data: dict):
|
||||||
|
# method = "textDocument/didChange"
|
||||||
|
# params = didchange_notification_range["params"]
|
||||||
|
|
||||||
|
# params["textDocument"]["uri"] = data["uri"]
|
||||||
|
# params["textDocument"]["languageId"] = data["language_id"]
|
||||||
|
# params["textDocument"]["version"] = data["version"]
|
||||||
|
|
||||||
|
# contentChanges = params["contentChanges"][0]
|
||||||
|
# start = contentChanges["range"]["start"]
|
||||||
|
# end = contentChanges["range"]["end"]
|
||||||
|
# contentChanges["text"] = data["text"]
|
||||||
|
# start["line"] = data["line"]
|
||||||
|
# start["character"] = 0
|
||||||
|
# end["line"] = data["line"]
|
||||||
|
# end["character"] = data["column"]
|
||||||
|
|
||||||
|
# self.send_notification( method, params )
|
||||||
|
|
||||||
|
def _lsp_definition(self, data: dict):
|
||||||
|
method = "textDocument/definition"
|
||||||
|
params = definition_request["params"]
|
||||||
|
|
||||||
|
params["textDocument"]["uri"] = data["uri"]
|
||||||
|
params["textDocument"]["languageId"] = data["language_id"]
|
||||||
|
params["textDocument"]["version"] = data["version"]
|
||||||
|
params["position"]["line"] = data["line"]
|
||||||
|
params["position"]["character"] = data["column"]
|
||||||
|
|
||||||
|
self.send_request( method, params )
|
||||||
|
|
||||||
|
def _lsp_completion(self, data: dict):
|
||||||
|
method = "textDocument/completion"
|
||||||
|
params = completion_request["params"]
|
||||||
|
|
||||||
|
params["textDocument"]["uri"] = data["uri"]
|
||||||
|
params["textDocument"]["languageId"] = data["language_id"]
|
||||||
|
params["textDocument"]["version"] = data["version"]
|
||||||
|
params["position"]["line"] = data["line"]
|
||||||
|
params["position"]["character"] = data["column"]
|
||||||
|
|
||||||
|
self.send_request( method, params )
|
||||||
|
|
||||||
|
def _lsp_java_class_file_contents(self, uri: str):
|
||||||
|
method = "java/classFileContents"
|
||||||
|
params = {
|
||||||
|
"uri": uri
|
||||||
|
}
|
||||||
|
|
||||||
|
self.send_request( method, params )
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Python imports
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
from gi.repository import GLib
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
# from libs import websockets
|
||||||
|
from ..dto.code.lsp.lsp_messages import get_message_str, get_message_obj
|
||||||
|
from ..dto.code.lsp.lsp_message_structs import \
|
||||||
|
LSPResponseTypes, ClientRequest, ClientNotification, \
|
||||||
|
LSPResponseRequest, LSPResponseNotification, LSPIDResponseNotification
|
||||||
|
|
||||||
|
from .lsp_client_base import LSPClientBase
|
||||||
|
from .websocket_client import WebsocketClient
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class LSPClientWebsocket(LSPClientBase):
|
||||||
|
def _send_message(self, data: ClientRequest | ClientNotification):
|
||||||
|
if not data: return
|
||||||
|
|
||||||
|
message_str = get_message_str(data)
|
||||||
|
message_size = len(message_str)
|
||||||
|
message = f"Content-Length: {message_size}\r\n\r\n{message_str}"
|
||||||
|
|
||||||
|
logger.debug(f"Client: {message_str}")
|
||||||
|
self.ws_client.send(message_str)
|
||||||
|
|
||||||
|
def start_client(self):
|
||||||
|
self.ws_client = WebsocketClient()
|
||||||
|
self.ws_client.set_socket(self._socket)
|
||||||
|
self.ws_client.set_callback(self._monitor_lsp_response)
|
||||||
|
self.ws_client.start_client()
|
||||||
|
|
||||||
|
return self.ws_client
|
||||||
|
|
||||||
|
def stop_client(self):
|
||||||
|
if not hasattr(self, "ws_client"): return
|
||||||
|
self.ws_client.close_client()
|
||||||
|
|
||||||
|
def _monitor_lsp_response(self, data: dict | None):
|
||||||
|
if not data: return
|
||||||
|
|
||||||
|
message = get_message_obj(data)
|
||||||
|
keys = message.keys()
|
||||||
|
lsp_response = None
|
||||||
|
|
||||||
|
if "result" in keys:
|
||||||
|
lsp_response = LSPResponseRequest(**get_message_obj(data))
|
||||||
|
|
||||||
|
if "method" in keys:
|
||||||
|
lsp_response = LSPResponseNotification(**get_message_obj(data)) if not "id" in keys else LSPIDResponseNotification( **get_message_obj(data) )
|
||||||
|
|
||||||
|
if not lsp_response: return
|
||||||
|
|
||||||
|
GLib.idle_add(self.handle_lsp_response, lsp_response)
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# Python imports
|
||||||
|
import json
|
||||||
|
import threading
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from ..libs import websocket
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class WebsocketClient:
|
||||||
|
def __init__(self):
|
||||||
|
self.ws = None
|
||||||
|
self._socket = None
|
||||||
|
self._connected = threading.Event()
|
||||||
|
|
||||||
|
|
||||||
|
def set_socket(self, socket: str):
|
||||||
|
self._socket = socket
|
||||||
|
|
||||||
|
def unset_socket(self):
|
||||||
|
self._socket = None
|
||||||
|
|
||||||
|
def send(self, message: str):
|
||||||
|
self.ws.send(message)
|
||||||
|
|
||||||
|
def on_message(self, ws, message: dict):
|
||||||
|
self.respond(message)
|
||||||
|
|
||||||
|
def on_error(self, ws, error: str):
|
||||||
|
logger.debug(f"WS Error: {error}")
|
||||||
|
|
||||||
|
def on_close(self, ws, close_status_code: int, close_msg: str):
|
||||||
|
logger.debug("WS Closed...")
|
||||||
|
|
||||||
|
def on_open(self, ws):
|
||||||
|
self._connected.set()
|
||||||
|
logger.debug("WS opened connection...")
|
||||||
|
|
||||||
|
def wait_for_connection(self, timeout: float = 5.0) -> bool:
|
||||||
|
return self._connected.wait(timeout)
|
||||||
|
|
||||||
|
def set_callback(self, callback: object):
|
||||||
|
self.respond = callback
|
||||||
|
|
||||||
|
def close_client(self):
|
||||||
|
self.ws.close()
|
||||||
|
|
||||||
|
@daemon_threaded
|
||||||
|
def start_client(self):
|
||||||
|
if not self._socket:
|
||||||
|
raise Exception("Socket address isn't set so cannot start WebsocketClient listener...")
|
||||||
|
|
||||||
|
# websocket.enableTrace(True)
|
||||||
|
self.ws = websocket.WebSocketApp(self._socket,
|
||||||
|
on_open = self.on_open,
|
||||||
|
on_message = self.on_message,
|
||||||
|
on_error = self.on_error,
|
||||||
|
on_close = self.on_close)
|
||||||
|
|
||||||
|
self.ws.run_forever(reconnect = 0.5)
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
{
|
||||||
|
"_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize",
|
||||||
|
"processId": "os.getpid()",
|
||||||
|
"clientInfo": {
|
||||||
|
"name": "LSP Manager",
|
||||||
|
"version": "0.0.1"
|
||||||
|
},
|
||||||
|
"locale": "en",
|
||||||
|
"rootPath": "repository_absolute_path",
|
||||||
|
"rootUri": "pathlib.Path(repository_absolute_path).as_uri()",
|
||||||
|
"capabilities": {
|
||||||
|
"textDocument": {
|
||||||
|
"completion": {
|
||||||
|
"dynamicRegistration": true,
|
||||||
|
"contextSupport": true,
|
||||||
|
"completionItem": {
|
||||||
|
"snippetSupport": false,
|
||||||
|
"commitCharactersSupport": true,
|
||||||
|
"documentationFormat": [
|
||||||
|
"markdown",
|
||||||
|
"plaintext"
|
||||||
|
],
|
||||||
|
"deprecatedSupport": true,
|
||||||
|
"preselectSupport": true,
|
||||||
|
"tagSupport": {
|
||||||
|
"valueSet": [
|
||||||
|
1
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"insertReplaceSupport": false,
|
||||||
|
"resolveSupport": {
|
||||||
|
"properties": [
|
||||||
|
"documentation",
|
||||||
|
"detail",
|
||||||
|
"additionalTextEdits"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"insertTextModeSupport": {
|
||||||
|
"valueSet": [
|
||||||
|
1,
|
||||||
|
2
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"labelDetailsSupport": true
|
||||||
|
},
|
||||||
|
"insertTextMode": 2,
|
||||||
|
"completionItemKind": {
|
||||||
|
"valueSet": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
6,
|
||||||
|
7,
|
||||||
|
8,
|
||||||
|
9,
|
||||||
|
10,
|
||||||
|
11,
|
||||||
|
12,
|
||||||
|
13,
|
||||||
|
14,
|
||||||
|
15,
|
||||||
|
16,
|
||||||
|
17,
|
||||||
|
18,
|
||||||
|
19,
|
||||||
|
20,
|
||||||
|
21,
|
||||||
|
22,
|
||||||
|
23,
|
||||||
|
24,
|
||||||
|
25
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"completionList": {
|
||||||
|
"itemDefaults": [
|
||||||
|
"commitCharacters",
|
||||||
|
"editRange",
|
||||||
|
"insertTextFormat",
|
||||||
|
"insertTextMode"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hover": {
|
||||||
|
"dynamicRegistration": true,
|
||||||
|
"contentFormat": [
|
||||||
|
"markdown",
|
||||||
|
"plaintext"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"signatureHelp": {
|
||||||
|
"dynamicRegistration": true,
|
||||||
|
"signatureInformation": {
|
||||||
|
"documentationFormat": [
|
||||||
|
"markdown",
|
||||||
|
"plaintext"
|
||||||
|
],
|
||||||
|
"parameterInformation": {
|
||||||
|
"labelOffsetSupport": true
|
||||||
|
},
|
||||||
|
"activeParameterSupport": true
|
||||||
|
},
|
||||||
|
"contextSupport": true
|
||||||
|
},
|
||||||
|
"definition": {
|
||||||
|
"dynamicRegistration": true,
|
||||||
|
"linkSupport": true
|
||||||
|
},
|
||||||
|
"references": {
|
||||||
|
"dynamicRegistration": true
|
||||||
|
},
|
||||||
|
"typeDefinition": {
|
||||||
|
"dynamicRegistration": true,
|
||||||
|
"linkSupport": true
|
||||||
|
},
|
||||||
|
"implementation": {
|
||||||
|
"dynamicRegistration": true,
|
||||||
|
"linkSupport": true
|
||||||
|
},
|
||||||
|
"colorProvider": {
|
||||||
|
"dynamicRegistration": true
|
||||||
|
},
|
||||||
|
"declaration": {
|
||||||
|
"dynamicRegistration": true,
|
||||||
|
"linkSupport": true
|
||||||
|
},
|
||||||
|
"callHierarchy": {
|
||||||
|
"dynamicRegistration": true
|
||||||
|
},
|
||||||
|
"inlayHint": {
|
||||||
|
"dynamicRegistration": true,
|
||||||
|
"resolveSupport": {
|
||||||
|
"properties": [
|
||||||
|
"tooltip",
|
||||||
|
"textEdits",
|
||||||
|
"label.tooltip",
|
||||||
|
"label.location",
|
||||||
|
"label.command"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"diagnostic": {
|
||||||
|
"dynamicRegistration": true,
|
||||||
|
"relatedDocumentSupport": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"trace": "verbose",
|
||||||
|
"workspaceFolders": "[\n {\n \"uri\": pathlib.Path(repository_absolute_path).as_uri(),\n \"name\": os.path.basename(repository_absolute_path),\n }\n ]"
|
||||||
|
}
|
||||||
@@ -0,0 +1,365 @@
|
|||||||
|
{
|
||||||
|
"java": {
|
||||||
|
"info": "https://download.eclipse.org/jdtls/",
|
||||||
|
"info-init-options": "https://github.com/eclipse-jdtls/eclipse.jdt.ls/wiki/Running-the-JAVA-LS-server-from-the-command-line",
|
||||||
|
"info-import-build": "https://www.javahotchocolate.com/tutorials/build-path.html",
|
||||||
|
"info-external-class-paths": "https://github.com/eclipse-jdtls/eclipse.jdt.ls/issues/3291",
|
||||||
|
"link": "https://download.eclipse.org/jdtls/milestones/?d",
|
||||||
|
"command": "lsp-ws-proxy --listen 4114 -- jdtls",
|
||||||
|
"alt-command": "lsp-ws-proxy -- jdtls",
|
||||||
|
"alt-command2": "java-language-server",
|
||||||
|
"socket": "ws://127.0.0.1:9999/java",
|
||||||
|
"socket-two": "ws://127.0.0.1:9999/?name=jdtls",
|
||||||
|
"alt-socket": "ws://127.0.0.1:9999/?name=java-language-server",
|
||||||
|
"initialization-options": {
|
||||||
|
"bundles": [
|
||||||
|
"intellicode-core.jar"
|
||||||
|
],
|
||||||
|
"workspaceFolders": [
|
||||||
|
"file://{workspace.folder}"
|
||||||
|
],
|
||||||
|
"extendedClientCapabilities": {
|
||||||
|
"classFileContentsSupport": true,
|
||||||
|
"executeClientCommandSupport": false
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"java": {
|
||||||
|
"autobuild": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"jdt": {
|
||||||
|
"ls": {
|
||||||
|
"javac": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"java": {
|
||||||
|
"home": "{user.home}/Portable_Apps/sdks/javasdk/jdk-22.0.2"
|
||||||
|
},
|
||||||
|
"lombokSupport": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"protobufSupport":{
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"androidSupport": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"updateBuildConfiguration": "automatic",
|
||||||
|
"maven": {
|
||||||
|
"userSettings": "{user.home}/.config/jdtls/settings.xml",
|
||||||
|
"globalSettings": "{user.home}/.config/jdtls/settings.xml"
|
||||||
|
},
|
||||||
|
"runtimes": [
|
||||||
|
{
|
||||||
|
"name": "JavaSE-17",
|
||||||
|
"path": "/usr/lib/jvm/java-17-openjdk",
|
||||||
|
"javadoc": "https://docs.oracle.com/en/java/javase/17/docs/api/",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "JavaSE-22",
|
||||||
|
"path": "{user.home}/Portable_Apps/sdks/javasdk/jdk-22.0.2",
|
||||||
|
"javadoc": "https://docs.oracle.com/en/java/javase/22/docs/api/",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"classPath": [
|
||||||
|
"{user.home}/.config/jdtls/m2/repository/**/*-sources.jar",
|
||||||
|
"lib/**/*-sources.jar"
|
||||||
|
],
|
||||||
|
"docPath": [
|
||||||
|
"{user.home}/.config/jdtls/m2/repository/**/*-javadoc.jar",
|
||||||
|
"lib/**/*-javadoc.jar"
|
||||||
|
],
|
||||||
|
"project": {
|
||||||
|
"encoding": "ignore",
|
||||||
|
"outputPath": "bin",
|
||||||
|
"referencedLibraries": [
|
||||||
|
"{user.home}/.config/jdtls/m2/repository/**/*.jar",
|
||||||
|
"lib/**/*.jar"
|
||||||
|
],
|
||||||
|
"importOnFirstTimeStartup": "automatic",
|
||||||
|
"importHint": true,
|
||||||
|
"resourceFilters": [
|
||||||
|
"node_modules",
|
||||||
|
"\\.git"
|
||||||
|
],
|
||||||
|
"sourcePaths": [
|
||||||
|
"src",
|
||||||
|
"{user.home}/.config/jdtls/m2/repository/**/*.jar"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sources": {
|
||||||
|
"organizeImports": {
|
||||||
|
"starThreshold": 99,
|
||||||
|
"staticStarThreshold": 99
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"imports": {
|
||||||
|
"gradle": {
|
||||||
|
"wrapper": {
|
||||||
|
"checksums": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"import": {
|
||||||
|
"maven": {
|
||||||
|
"enabled": true,
|
||||||
|
"offline": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"disableTestClasspathFlag": false
|
||||||
|
},
|
||||||
|
"gradle": {
|
||||||
|
"enabled": false,
|
||||||
|
"wrapper": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"version": "",
|
||||||
|
"home": "abs(static/gradle-7.3.3)",
|
||||||
|
"java": {
|
||||||
|
"home": "abs(static/launch_jres/17.0.6-linux-x86_64)"
|
||||||
|
},
|
||||||
|
"offline": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"arguments": [],
|
||||||
|
"jvmArguments": [],
|
||||||
|
"user": {
|
||||||
|
"home": ""
|
||||||
|
},
|
||||||
|
"annotationProcessing": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclusions": [
|
||||||
|
"**/node_modules/**",
|
||||||
|
"**/.metadata/**",
|
||||||
|
"**/archetype-resources/**",
|
||||||
|
"**/META-INF/maven/**"
|
||||||
|
],
|
||||||
|
"generatesMetadataFilesAtProjectRoot": false
|
||||||
|
},
|
||||||
|
"maven": {
|
||||||
|
"downloadSources": true,
|
||||||
|
"updateSnapshots": true
|
||||||
|
},
|
||||||
|
"silentNotification": true,
|
||||||
|
"contentProvider": {
|
||||||
|
"preferred": "fernflower"
|
||||||
|
},
|
||||||
|
"signatureHelp": {
|
||||||
|
"enabled": true,
|
||||||
|
"description": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"completion": {
|
||||||
|
"enabled": true,
|
||||||
|
"engine": "ecj",
|
||||||
|
"matchCase": "firstletter",
|
||||||
|
"maxResults": 25,
|
||||||
|
"guessMethodArguments": true,
|
||||||
|
"lazyResolveTextEdit": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"postfix": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"favoriteStaticMembers": [
|
||||||
|
"org.junit.Assert.*",
|
||||||
|
"org.junit.Assume.*",
|
||||||
|
"org.junit.jupiter.api.Assertions.*",
|
||||||
|
"org.junit.jupiter.api.Assumptions.*",
|
||||||
|
"org.junit.jupiter.api.DynamicContainer.*",
|
||||||
|
"org.junit.jupiter.api.DynamicTest.*"
|
||||||
|
],
|
||||||
|
"importOrder": [
|
||||||
|
"#",
|
||||||
|
"java",
|
||||||
|
"javax",
|
||||||
|
"org",
|
||||||
|
"com"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"references": {
|
||||||
|
"includeAccessors": true,
|
||||||
|
"includeDecompiledSources": true
|
||||||
|
},
|
||||||
|
"codeGeneration": {
|
||||||
|
"toString": {
|
||||||
|
"template": "${object.className}{${member.name()}=${member.value}, ${otherMembers}}"
|
||||||
|
},
|
||||||
|
"insertionLocation": "afterCursor",
|
||||||
|
"useBlocks": true
|
||||||
|
},
|
||||||
|
"implementationsCodeLens": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"referencesCodeLens": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"progressReports": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"saveActions": {
|
||||||
|
"organizeImports": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"python": {
|
||||||
|
"info": "https://github.com/python-lsp/python-lsp-server",
|
||||||
|
"command": "lsp-ws-proxy -- pylsp",
|
||||||
|
"alt-command": "pylsp",
|
||||||
|
"alt-command2": "lsp-ws-proxy --listen 4114 -- pylsp",
|
||||||
|
"alt-command3": "pylsp --ws --port 4114",
|
||||||
|
"socket": "ws://127.0.0.1:9999/python",
|
||||||
|
"socket-two": "ws://127.0.0.1:9999/?name=pylsp",
|
||||||
|
"initialization-options": {
|
||||||
|
"pylsp": {
|
||||||
|
"rope": {
|
||||||
|
"ropeFolder": "{user.home}/.config/newton/lsps/ropeproject"
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"ruff": {
|
||||||
|
"enabled": true,
|
||||||
|
"extendSelect": ["I"],
|
||||||
|
"lineLength": 80
|
||||||
|
},
|
||||||
|
"pycodestyle": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"pyflakes": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"pylint": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"mccabe": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"pylsp_rope": {
|
||||||
|
"rename": false
|
||||||
|
},
|
||||||
|
"rope_rename": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"rope_autoimport": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"rope_completion": {
|
||||||
|
"enabled": false,
|
||||||
|
"eager": false
|
||||||
|
},
|
||||||
|
"jedi_rename": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"jedi_completion": {
|
||||||
|
"enabled": true,
|
||||||
|
"include_class_objects": true,
|
||||||
|
"include_function_objects": true,
|
||||||
|
"fuzzy": false
|
||||||
|
},
|
||||||
|
"jedi": {
|
||||||
|
"root_dir": "file://{workspace.folder}",
|
||||||
|
"extra_paths": [
|
||||||
|
"{user.home}/Portable_Apps/py-venvs/pylsp-venv/venv/lib/python3.10/site-packages"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"python - jedi-language-server": {
|
||||||
|
"hidden": true,
|
||||||
|
"info": "https://pypi.org/project/jedi-language-server/",
|
||||||
|
"command": "jedi-language-server",
|
||||||
|
"alt-command": "lsp-ws-proxy --listen 3030 -- jedi-language-server",
|
||||||
|
"socket": "ws://127.0.0.1:9999/python",
|
||||||
|
"socket-two": "ws://127.0.0.1:9999/?name=jedi-language-server",
|
||||||
|
"initialization-options": {
|
||||||
|
"jediSettings": {
|
||||||
|
"autoImportModules": [],
|
||||||
|
"caseInsensitiveCompletion": true,
|
||||||
|
"debug": false
|
||||||
|
},
|
||||||
|
"completion": {
|
||||||
|
"disableSnippets": false,
|
||||||
|
"resolveEagerly": false,
|
||||||
|
"ignorePatterns": []
|
||||||
|
},
|
||||||
|
"markupKindPreferred": "markdown",
|
||||||
|
"workspace": {
|
||||||
|
"extraPaths": [
|
||||||
|
"{user.home}/Portable_Apps/py-venvs/pylsp-venv/venv/lib/python3.10/site-packages"
|
||||||
|
],
|
||||||
|
"environmentPath": "{user.home}/Portable_Apps/py-venvs/gtk-apps-venv/venv/bin/python",
|
||||||
|
"symbols": {
|
||||||
|
"ignoreFolders": [
|
||||||
|
".nox",
|
||||||
|
".tox",
|
||||||
|
".venv",
|
||||||
|
"__pycache__",
|
||||||
|
"venv"
|
||||||
|
],
|
||||||
|
"maxSymbols": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cpp": {
|
||||||
|
"info": "https://clangd.llvm.org/",
|
||||||
|
"command": "lsp-ws-proxy -- clangd",
|
||||||
|
"alt-command": "clangd",
|
||||||
|
"socket": "ws://127.0.0.1:9999/cpp",
|
||||||
|
"socket-two": "ws://127.0.0.1:9999/?name=clangd",
|
||||||
|
"initialization-options": {}
|
||||||
|
},
|
||||||
|
"c": {
|
||||||
|
"hidden": true,
|
||||||
|
"info": "https://clangd.llvm.org/",
|
||||||
|
"command": "lsp-ws-proxy -- clangd",
|
||||||
|
"alt-command": "clangd",
|
||||||
|
"socket": "ws://127.0.0.1:9999/c",
|
||||||
|
"socket-two": "ws://127.0.0.1:9999/?name=clangd",
|
||||||
|
"initialization-options": {}
|
||||||
|
},
|
||||||
|
"go": {
|
||||||
|
"info": "https://pkg.go.dev/golang.org/x/tools/gopls#section-readme",
|
||||||
|
"command": "lsp-ws-proxy -- gopls",
|
||||||
|
"alt-command": "gopls",
|
||||||
|
"socket": "ws://127.0.0.1:9999/go",
|
||||||
|
"socket-two": "ws://127.0.0.1:9999/?name=gopls",
|
||||||
|
"initialization-options": {}
|
||||||
|
},
|
||||||
|
"typescript": {
|
||||||
|
"info": "https://github.com/typescript-language-server/typescript-language-server",
|
||||||
|
"command": "lsp-ws-proxy -- typescript-language-server",
|
||||||
|
"alt-command": "typescript-language-server --stdio",
|
||||||
|
"socket": "ws://127.0.0.1:9999/typescript",
|
||||||
|
"socket-two": "ws://127.0.0.1:9999/?name=ts",
|
||||||
|
"initialization-options": {}
|
||||||
|
},
|
||||||
|
"sh": {
|
||||||
|
"info": "",
|
||||||
|
"command": "",
|
||||||
|
"alt-command": "",
|
||||||
|
"socket": "ws://127.0.0.1:9999/bash",
|
||||||
|
"socket-two": "ws://127.0.0.1:9999/?name=shell",
|
||||||
|
"initialization-options": {}
|
||||||
|
},
|
||||||
|
"lua": {
|
||||||
|
"info": "https://github.com/LuaLS/lua-language-server",
|
||||||
|
"command": "lsp-ws-proxy -- lua-language-server",
|
||||||
|
"alt-command": "lua-language-server",
|
||||||
|
"socket": "ws://127.0.0.1:9999/lua",
|
||||||
|
"socket-two": "ws://127.0.0.1:9999/?name=lua",
|
||||||
|
"initialization-options": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
"""
|
||||||
|
Libs Code DTO(s) Events Package
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .lsp_event import LspEvent
|
||||||
|
|
||||||
|
from .register_lsp_client_event import RegisterLspClientEvent
|
||||||
|
from .unregister_lsp_client_event import UnregisterLspClientEvent
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Python imports
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from libs.dto.code.events import CodeEvent
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LspEvent(CodeEvent):
|
||||||
|
...
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Python imports
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from ....response_handlers.base_handler import BaseHandler
|
||||||
|
|
||||||
|
from .lsp_event import LspEvent
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RegisterLspClientEvent(LspEvent):
|
||||||
|
lang_id: str = ""
|
||||||
|
lang_config: str = "{}"
|
||||||
|
handler: BaseHandler = None
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Python imports
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from .lsp_event import LspEvent
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UnregisterLspClientEvent(LspEvent):
|
||||||
|
lang_id: str = ""
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# Python imports
|
||||||
|
import json
|
||||||
|
import enum
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from .lsp_structs import TextDocumentItem
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class MessageEncoder(json.JSONEncoder):
|
||||||
|
"""
|
||||||
|
Encodes an object in JSON
|
||||||
|
"""
|
||||||
|
|
||||||
|
def default(self, o): # pylint: disable=E0202
|
||||||
|
return o.__dict__
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ClientRequest(object):
|
||||||
|
def __init__(self, id: int, method: str, params: dict):
|
||||||
|
"""
|
||||||
|
Constructs a new Client Request instance.
|
||||||
|
|
||||||
|
:param int id: Message id to track instance.
|
||||||
|
:param str method: The type of lsp request being made.
|
||||||
|
:param dict params: The arguments of the given method.
|
||||||
|
"""
|
||||||
|
self.jsonrpc = "2.0"
|
||||||
|
self.id = id
|
||||||
|
self.method = method
|
||||||
|
self.params = params
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ClientNotification(object):
|
||||||
|
def __init__(self, method: str, params: dict):
|
||||||
|
"""
|
||||||
|
Constructs a new Client Notification instance.
|
||||||
|
|
||||||
|
:param str method: The type of lsp notification being made.
|
||||||
|
:param dict params: The arguments of the given method.
|
||||||
|
"""
|
||||||
|
self.jsonrpc = "2.0"
|
||||||
|
self.method = method
|
||||||
|
self.params = params
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LSPResponseRequest(object):
|
||||||
|
"""
|
||||||
|
Constructs a new LSP Response Request instance.
|
||||||
|
|
||||||
|
:param id result: The id of the given message.
|
||||||
|
:param dict result: The arguments of the given method.
|
||||||
|
"""
|
||||||
|
jsonrpc: str
|
||||||
|
id: int
|
||||||
|
result: dict
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LSPResponseNotification(object):
|
||||||
|
"""
|
||||||
|
Constructs a new LSP Response Notification instance.
|
||||||
|
|
||||||
|
:param str method: The type of lsp notification being made.
|
||||||
|
:params dict result: The arguments of the given method.
|
||||||
|
"""
|
||||||
|
jsonrpc: str
|
||||||
|
method: str
|
||||||
|
params: dict
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LSPIDResponseNotification(object):
|
||||||
|
"""
|
||||||
|
Constructs a new LSP Response Notification instance.
|
||||||
|
|
||||||
|
:param str method: The type of lsp notification being made.
|
||||||
|
:params dict result: The arguments of the given method.
|
||||||
|
"""
|
||||||
|
jsonrpc: str
|
||||||
|
id: int
|
||||||
|
method: str
|
||||||
|
params: dict
|
||||||
|
|
||||||
|
|
||||||
|
class MessageTypes(ClientRequest, ClientNotification, LSPResponseRequest, LSPResponseNotification, LSPIDResponseNotification):
|
||||||
|
...
|
||||||
|
|
||||||
|
class ClientMessageTypes(ClientRequest, ClientNotification):
|
||||||
|
...
|
||||||
|
|
||||||
|
class LSPResponseTypes(LSPResponseRequest, LSPResponseNotification):
|
||||||
|
...
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
# Python imports
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from .lsp_message_structs import MessageEncoder
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
LEN_HEADER = "Content-Length: "
|
||||||
|
TYPE_HEADER = "Content-Type: "
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def get_message_str(data: dict) -> str:
|
||||||
|
return json.dumps(data, separators = (',', ':'), indent = 4, cls = MessageEncoder)
|
||||||
|
|
||||||
|
def get_message_obj(data: str):
|
||||||
|
return json.loads(data)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Request type formatting
|
||||||
|
# https://github.com/microsoft/multilspy/blob/main/src/multilspy/language_server.py#L417
|
||||||
|
content_part = {
|
||||||
|
"method": "textDocument/definition",
|
||||||
|
"params": {
|
||||||
|
"textDocument": {
|
||||||
|
"uri": "file://",
|
||||||
|
"languageId": "python",
|
||||||
|
"version": 1,
|
||||||
|
"text": ""
|
||||||
|
},
|
||||||
|
"position": {
|
||||||
|
"line": 5,
|
||||||
|
"character": 12,
|
||||||
|
"offset": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
didopen_notification = {
|
||||||
|
"method": "textDocument/didOpen",
|
||||||
|
"params": {
|
||||||
|
"textDocument": {
|
||||||
|
"uri": "file://",
|
||||||
|
"languageId": "python",
|
||||||
|
"version": 1,
|
||||||
|
"text": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
didsave_notification = {
|
||||||
|
"method": "textDocument/didSave",
|
||||||
|
"params": {
|
||||||
|
"textDocument": {
|
||||||
|
"uri": "file://"
|
||||||
|
},
|
||||||
|
"text": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
didclose_notification = {
|
||||||
|
"method": "textDocument/didClose",
|
||||||
|
"params": {
|
||||||
|
"textDocument": {
|
||||||
|
"uri": "file://"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
didchange_notification = {
|
||||||
|
"method": "textDocument/didChange",
|
||||||
|
"params": {
|
||||||
|
"textDocument": {
|
||||||
|
"uri": "file://",
|
||||||
|
"languageId": "python",
|
||||||
|
"version": 1,
|
||||||
|
},
|
||||||
|
"contentChanges": [
|
||||||
|
{
|
||||||
|
"text": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
didchange_notification_range = {
|
||||||
|
"method": "textDocument/didChange",
|
||||||
|
"params": {
|
||||||
|
"textDocument": {
|
||||||
|
"uri": "file://",
|
||||||
|
"languageId": "python",
|
||||||
|
"version": 1,
|
||||||
|
"text": ""
|
||||||
|
},
|
||||||
|
"contentChanges": [
|
||||||
|
{
|
||||||
|
"range": {
|
||||||
|
"start": {
|
||||||
|
"line": 1,
|
||||||
|
"character": 1,
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 1,
|
||||||
|
"character": 1,
|
||||||
|
},
|
||||||
|
"rangeLength": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# CompletionTriggerKind = 1 | 2 | 3;
|
||||||
|
# export const Invoked: 1 = 1;
|
||||||
|
# export const TriggerCharacter: 2 = 2;
|
||||||
|
# export const TriggerForIncompleteCompletions: 3 = 3;
|
||||||
|
completion_request = {
|
||||||
|
"method": "textDocument/completion",
|
||||||
|
"params": {
|
||||||
|
"textDocument": {
|
||||||
|
"uri": "file://",
|
||||||
|
"languageId": "python",
|
||||||
|
"version": 1,
|
||||||
|
"text": ""
|
||||||
|
},
|
||||||
|
"position": {
|
||||||
|
"line": 5,
|
||||||
|
"character": 12,
|
||||||
|
"offset": 0
|
||||||
|
},
|
||||||
|
"contet": {
|
||||||
|
"triggerKind": 3,
|
||||||
|
"triggerCharacter": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
definition_request = {
|
||||||
|
"method": "textDocument/definition",
|
||||||
|
"params": {
|
||||||
|
"textDocument": {
|
||||||
|
"uri": "file://",
|
||||||
|
"languageId": "python",
|
||||||
|
"version": 1,
|
||||||
|
"text": ""
|
||||||
|
},
|
||||||
|
"position": {
|
||||||
|
"line": 5,
|
||||||
|
"character": 12,
|
||||||
|
"offset": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
references_request = {
|
||||||
|
"method": "textDocument/references",
|
||||||
|
"params": {
|
||||||
|
"context": {
|
||||||
|
"includeDeclaration": False
|
||||||
|
},
|
||||||
|
"textDocument": {
|
||||||
|
"uri": "file://",
|
||||||
|
"languageId": "python",
|
||||||
|
"version": 1,
|
||||||
|
"text": ""
|
||||||
|
},
|
||||||
|
"position": {
|
||||||
|
"line": 30,
|
||||||
|
"character": 13,
|
||||||
|
"offset": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
symbols_request = {
|
||||||
|
"method": "textDocument/documentSymbol",
|
||||||
|
"params": {
|
||||||
|
"textDocument": {
|
||||||
|
"uri": "file://",
|
||||||
|
"languageId": "python",
|
||||||
|
"version": 1,
|
||||||
|
"text": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,571 @@
|
|||||||
|
# Python imports
|
||||||
|
import enum
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def to_type(o, new_type):
|
||||||
|
'''
|
||||||
|
Helper funciton that receives an object or a dict and convert it to a new
|
||||||
|
given type.
|
||||||
|
|
||||||
|
:param object|dict o: The object to convert
|
||||||
|
:param Type new_type: The type to convert to.
|
||||||
|
'''
|
||||||
|
|
||||||
|
return o if new_type == type(o) else new_type(**o)
|
||||||
|
|
||||||
|
|
||||||
|
class Position(object):
|
||||||
|
def __init__(self, line, character):
|
||||||
|
"""
|
||||||
|
Constructs a new Position instance.
|
||||||
|
|
||||||
|
:param int line: Line position in a document (zero-based).
|
||||||
|
:param int character: Character offset on a line in a document
|
||||||
|
(zero-based).
|
||||||
|
"""
|
||||||
|
self.line = line
|
||||||
|
self.character = character
|
||||||
|
|
||||||
|
|
||||||
|
class Range(object):
|
||||||
|
def __init__(self, start, end):
|
||||||
|
"""
|
||||||
|
Constructs a new Range instance.
|
||||||
|
|
||||||
|
:param Position start: The range's start position.
|
||||||
|
:param Position end: The range's end position.
|
||||||
|
"""
|
||||||
|
self.start = to_type(start, Position)
|
||||||
|
self.end = to_type(end, Position)
|
||||||
|
|
||||||
|
|
||||||
|
class Location(object):
|
||||||
|
"""
|
||||||
|
Represents a location inside a resource, such as a line inside a text file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, uri, range):
|
||||||
|
"""
|
||||||
|
Constructs a new Location instance.
|
||||||
|
|
||||||
|
:param str uri: Resource file.
|
||||||
|
:param Range range: The range inside the file
|
||||||
|
"""
|
||||||
|
self.uri = uri
|
||||||
|
self.range = to_type(range, Range)
|
||||||
|
|
||||||
|
|
||||||
|
class LocationLink(object):
|
||||||
|
"""
|
||||||
|
Represents a link between a source and a target location.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, originSelectionRange, targetUri, targetRange, targetSelectionRange):
|
||||||
|
"""
|
||||||
|
Constructs a new LocationLink instance.
|
||||||
|
|
||||||
|
:param Range originSelectionRange: Span of the origin of this link.
|
||||||
|
Used as the underlined span for mouse interaction. Defaults to the word range at the mouse position.
|
||||||
|
:param str targetUri: The target resource identifier of this link.
|
||||||
|
:param Range targetRange: The full target range of this link. If the target for example is a symbol then target
|
||||||
|
range is the range enclosing this symbol not including leading/trailing whitespace but everything else
|
||||||
|
like comments. This information is typically used to highlight the range in the editor.
|
||||||
|
:param Range targetSelectionRange: The range that should be selected and revealed when this link is being followed,
|
||||||
|
e.g the name of a function. Must be contained by the the `targetRange`. See also `DocumentSymbol#range`
|
||||||
|
"""
|
||||||
|
self.originSelectionRange = to_type(originSelectionRange, Range)
|
||||||
|
self.targetUri = targetUri
|
||||||
|
self.targetRange = to_type(targetRange, Range)
|
||||||
|
self.targetSelectionRange = to_type(targetSelectionRange, Range)
|
||||||
|
|
||||||
|
|
||||||
|
class Diagnostic(object):
|
||||||
|
def __init__(self, range, severity, code, source, message, relatedInformation):
|
||||||
|
"""
|
||||||
|
Constructs a new Diagnostic instance.
|
||||||
|
:param Range range: The range at which the message applies.Resource file.
|
||||||
|
:param int severity: The diagnostic's severity. Can be omitted. If omitted it is up to the
|
||||||
|
client to interpret diagnostics as error, warning, info or hint.
|
||||||
|
:param str code: The diagnostic's code, which might appear in the user interface.
|
||||||
|
:param str source: A human-readable string describing the source of this
|
||||||
|
diagnostic, e.g. 'typescript' or 'super lint'.
|
||||||
|
:param str message: The diagnostic's message.
|
||||||
|
:param list relatedInformation: An array of related diagnostic information, e.g. when symbol-names within
|
||||||
|
a scope collide all definitions can be marked via this property.
|
||||||
|
"""
|
||||||
|
self.range = range
|
||||||
|
self.severity = severity
|
||||||
|
self.code = code
|
||||||
|
self.source = source
|
||||||
|
self.message = message
|
||||||
|
self.relatedInformation = relatedInformation
|
||||||
|
|
||||||
|
|
||||||
|
class DiagnosticSeverity(object):
|
||||||
|
Error = 1
|
||||||
|
Warning = 2 # TODO: warning is known in python
|
||||||
|
Information = 3
|
||||||
|
Hint = 4
|
||||||
|
|
||||||
|
|
||||||
|
class DiagnosticRelatedInformation(object):
|
||||||
|
def __init__(self, location, message):
|
||||||
|
"""
|
||||||
|
Constructs a new Diagnostic instance.
|
||||||
|
:param Location location: The location of this related diagnostic information.
|
||||||
|
:param str message: The message of this related diagnostic information.
|
||||||
|
"""
|
||||||
|
self.location = location
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
|
||||||
|
class Command(object):
|
||||||
|
def __init__(self, title, command, arguments):
|
||||||
|
"""
|
||||||
|
Constructs a new Diagnostic instance.
|
||||||
|
:param str title: Title of the command, like `save`.
|
||||||
|
:param str command: The identifier of the actual command handler.
|
||||||
|
:param list argusments: Arguments that the command handler should be invoked with.
|
||||||
|
"""
|
||||||
|
self.title = title
|
||||||
|
self.command = command
|
||||||
|
self.arguments = arguments
|
||||||
|
|
||||||
|
|
||||||
|
class TextDocumentItem(object):
|
||||||
|
"""
|
||||||
|
An item to transfer a text document from the client to the server.
|
||||||
|
"""
|
||||||
|
def __init__(self, uri, languageId, version, text):
|
||||||
|
"""
|
||||||
|
Constructs a new Diagnostic instance.
|
||||||
|
|
||||||
|
:param DocumentUri uri: uri file path.
|
||||||
|
:param str languageId: The identifier of the actual command handler.
|
||||||
|
:param int version: Arguments that the command handler should be invoked with.
|
||||||
|
:param str text: Arguments that the command handler should be invoked with.
|
||||||
|
"""
|
||||||
|
self.uri = uri
|
||||||
|
self.languageId = languageId
|
||||||
|
self.version = version
|
||||||
|
self.text = text
|
||||||
|
|
||||||
|
|
||||||
|
class TextDocumentIdentifier(object):
|
||||||
|
"""
|
||||||
|
Text documents are identified using a URI. On the protocol level, URIs are passed as strings.
|
||||||
|
"""
|
||||||
|
def __init__(self, uri):
|
||||||
|
"""
|
||||||
|
Constructs a new TextDocumentIdentifier instance.
|
||||||
|
|
||||||
|
:param DocumentUri uri: The text document's URI.
|
||||||
|
"""
|
||||||
|
self.uri = uri
|
||||||
|
|
||||||
|
|
||||||
|
class VersionedTextDocumentIdentifier(TextDocumentIdentifier):
|
||||||
|
"""
|
||||||
|
An identifier to denote a specific version of a text document.
|
||||||
|
"""
|
||||||
|
def __init__(self, uri, version):
|
||||||
|
"""
|
||||||
|
Constructs a new TextDocumentIdentifier instance.
|
||||||
|
|
||||||
|
:param DocumentUri uri: The text document's URI.
|
||||||
|
:param int version: The version number of this document. If a versioned
|
||||||
|
text document identifier is sent from the server to the client and
|
||||||
|
the file is not open in the editor (the server has not received an
|
||||||
|
open notification before) the server can send `null` to indicate
|
||||||
|
that the version is known and the content on disk is the truth (as
|
||||||
|
speced with document content ownership).
|
||||||
|
The version number of a document will increase after each change, including
|
||||||
|
undo/redo. The number doesn't need to be consecutive.
|
||||||
|
"""
|
||||||
|
super(VersionedTextDocumentIdentifier, self).__init__(uri)
|
||||||
|
self.version = version
|
||||||
|
|
||||||
|
|
||||||
|
class TextDocumentContentChangeEvent(object):
|
||||||
|
"""
|
||||||
|
An event describing a change to a text document. If range and rangeLength are omitted
|
||||||
|
the new text is considered to be the full content of the document.
|
||||||
|
"""
|
||||||
|
def __init__(self, range, rangeLength, text):
|
||||||
|
"""
|
||||||
|
Constructs a new TextDocumentContentChangeEvent instance.
|
||||||
|
|
||||||
|
:param Range range: The range of the document that changed.
|
||||||
|
:param int rangeLength: The length of the range that got replaced.
|
||||||
|
:param str text: The new text of the range/document.
|
||||||
|
"""
|
||||||
|
self.range = range
|
||||||
|
self.rangeLength = rangeLength
|
||||||
|
self.text = text
|
||||||
|
|
||||||
|
|
||||||
|
class TextDocumentPositionParams(object):
|
||||||
|
"""
|
||||||
|
A parameter literal used in requests to pass a text document and a position inside that document.
|
||||||
|
"""
|
||||||
|
def __init__(self, textDocument, position):
|
||||||
|
"""
|
||||||
|
Constructs a new TextDocumentPositionParams instance.
|
||||||
|
|
||||||
|
:param TextDocumentIdentifier textDocument: The text document.
|
||||||
|
:param Position position: The position inside the text document.
|
||||||
|
"""
|
||||||
|
self.textDocument = textDocument
|
||||||
|
self.position = position
|
||||||
|
|
||||||
|
|
||||||
|
class LANGUAGE_IDENTIFIER(object):
|
||||||
|
BAT = "bat"
|
||||||
|
BIBTEX = "bibtex"
|
||||||
|
CLOJURE = "clojure"
|
||||||
|
COFFESCRIPT = "coffeescript"
|
||||||
|
C = "c"
|
||||||
|
CPP = "cpp"
|
||||||
|
CSHARP = "csharp"
|
||||||
|
CSS = "css"
|
||||||
|
DIFF = "diff"
|
||||||
|
DOCKERFILE = "dockerfile"
|
||||||
|
FSHARP = "fsharp"
|
||||||
|
GIT_COMMIT = "git-commit"
|
||||||
|
GIT_REBASE = "git-rebase"
|
||||||
|
GO = "go"
|
||||||
|
GROOVY = "groovy"
|
||||||
|
HANDLEBARS = "handlebars"
|
||||||
|
HTML = "html"
|
||||||
|
INI = "ini"
|
||||||
|
JAVA = "java"
|
||||||
|
JAVASCRIPT = "javascript"
|
||||||
|
JSON = "json"
|
||||||
|
LATEX = "latex"
|
||||||
|
LESS = "less"
|
||||||
|
LUA = "lua"
|
||||||
|
MAKEFILE = "makefile"
|
||||||
|
MARKDOWN = "markdown"
|
||||||
|
OBJECTIVE_C = "objective-c"
|
||||||
|
OBJECTIVE_CPP = "objective-cpp"
|
||||||
|
Perl = "perl"
|
||||||
|
PHP = "php"
|
||||||
|
POWERSHELL = "powershell"
|
||||||
|
PUG = "jade"
|
||||||
|
PYTHON = "python"
|
||||||
|
R = "r"
|
||||||
|
RAZOR = "razor"
|
||||||
|
RUBY = "ruby"
|
||||||
|
RUST = "rust"
|
||||||
|
SASS = "sass"
|
||||||
|
SCSS = "scss"
|
||||||
|
ShaderLab = "shaderlab"
|
||||||
|
SHELL_SCRIPT = "shellscript"
|
||||||
|
SQL = "sql"
|
||||||
|
SWIFT = "swift"
|
||||||
|
TYPE_SCRIPT = "typescript"
|
||||||
|
TEX = "tex"
|
||||||
|
VB = "vb"
|
||||||
|
XML = "xml"
|
||||||
|
XSL = "xsl"
|
||||||
|
YAML = "yaml"
|
||||||
|
|
||||||
|
|
||||||
|
class SymbolKind(enum.Enum):
|
||||||
|
File = 1
|
||||||
|
Module = 2
|
||||||
|
Namespace = 3
|
||||||
|
Package = 4
|
||||||
|
Class = 5
|
||||||
|
Method = 6
|
||||||
|
Property = 7
|
||||||
|
Field = 8
|
||||||
|
Constructor = 9
|
||||||
|
Enum = 10
|
||||||
|
Interface = 11
|
||||||
|
Function = 12
|
||||||
|
Variable = 13
|
||||||
|
Constant = 14
|
||||||
|
String = 15
|
||||||
|
Number = 16
|
||||||
|
Boolean = 17
|
||||||
|
Array = 18
|
||||||
|
Object = 19
|
||||||
|
Key = 20
|
||||||
|
Null = 21
|
||||||
|
EnumMember = 22
|
||||||
|
Struct = 23
|
||||||
|
Event = 24
|
||||||
|
Operator = 25
|
||||||
|
TypeParameter = 26
|
||||||
|
|
||||||
|
|
||||||
|
class SymbolInformation(object):
|
||||||
|
"""
|
||||||
|
Represents information about programming constructs like variables, classes, interfaces etc.
|
||||||
|
"""
|
||||||
|
def __init__(self, name, kind, location, containerName = None, deprecated = False):
|
||||||
|
"""
|
||||||
|
Constructs a new SymbolInformation instance.
|
||||||
|
|
||||||
|
:param str name: The name of this symbol.
|
||||||
|
:param int kind: The kind of this symbol.
|
||||||
|
:param bool Location: The location of this symbol. The location's range is used by a tool
|
||||||
|
to reveal the location in the editor. If the symbol is selected in the
|
||||||
|
tool the range's start information is used to position the cursor. So
|
||||||
|
the range usually spans more then the actual symbol's name and does
|
||||||
|
normally include things like visibility modifiers.
|
||||||
|
|
||||||
|
The range doesn't have to denote a node range in the sense of a abstract
|
||||||
|
syntax tree. It can therefore not be used to re-construct a hierarchy of
|
||||||
|
the symbols.
|
||||||
|
:param str containerName: The name of the symbol containing this symbol. This information is for
|
||||||
|
user interface purposes (e.g. to render a qualifier in the user interface
|
||||||
|
if necessary). It can't be used to re-infer a hierarchy for the document
|
||||||
|
symbols.
|
||||||
|
:param bool deprecated: Indicates if this symbol is deprecated.
|
||||||
|
"""
|
||||||
|
self.name = name
|
||||||
|
self.kind = SymbolKind(kind)
|
||||||
|
self.deprecated = deprecated
|
||||||
|
self.location = to_type(location, Location)
|
||||||
|
self.containerName = containerName
|
||||||
|
|
||||||
|
|
||||||
|
class ParameterInformation(object):
|
||||||
|
"""
|
||||||
|
Represents a parameter of a callable-signature. A parameter can
|
||||||
|
have a label and a doc-comment.
|
||||||
|
"""
|
||||||
|
def __init__(self, label, documentation = ""):
|
||||||
|
"""
|
||||||
|
Constructs a new ParameterInformation instance.
|
||||||
|
|
||||||
|
:param str label: The label of this parameter. Will be shown in the UI.
|
||||||
|
:param str documentation: The human-readable doc-comment of this parameter. Will be shown in the UI but can be omitted.
|
||||||
|
"""
|
||||||
|
self.label = label
|
||||||
|
self.documentation = documentation
|
||||||
|
|
||||||
|
|
||||||
|
class SignatureInformation(object):
|
||||||
|
"""
|
||||||
|
Represents the signature of something callable. A signature
|
||||||
|
can have a label, like a function-name, a doc-comment, and
|
||||||
|
a set of parameters.
|
||||||
|
"""
|
||||||
|
def __init__(self, label, documentation = "", parameters = []):
|
||||||
|
"""
|
||||||
|
Constructs a new SignatureInformation instance.
|
||||||
|
|
||||||
|
:param str label: The label of this signature. Will be shown in the UI.
|
||||||
|
:param str documentation: The human-readable doc-comment of this signature. Will be shown in the UI but can be omitted.
|
||||||
|
:param ParameterInformation[] parameters: The parameters of this signature.
|
||||||
|
"""
|
||||||
|
self.label = label
|
||||||
|
self.documentation = documentation
|
||||||
|
self.parameters = [to_type(parameter, ParameterInformation) for parameter in parameters]
|
||||||
|
|
||||||
|
|
||||||
|
class SignatureHelp(object):
|
||||||
|
"""
|
||||||
|
Signature help represents the signature of something
|
||||||
|
callable. There can be multiple signature but only one
|
||||||
|
active and only one active parameter.
|
||||||
|
"""
|
||||||
|
def __init__(self, signatures, activeSignature = 0, activeParameter = 0):
|
||||||
|
"""
|
||||||
|
Constructs a new SignatureHelp instance.
|
||||||
|
|
||||||
|
:param SignatureInformation[] signatures: One or more signatures.
|
||||||
|
:param int activeSignature:
|
||||||
|
:param int activeParameter:
|
||||||
|
"""
|
||||||
|
self.signatures = [to_type(signature, SignatureInformation) for signature in signatures]
|
||||||
|
self.activeSignature = activeSignature
|
||||||
|
self.activeParameter = activeParameter
|
||||||
|
|
||||||
|
|
||||||
|
class CompletionTriggerKind(object):
|
||||||
|
Invoked = 1
|
||||||
|
TriggerCharacter = 2
|
||||||
|
TriggerForIncompleteCompletions = 3
|
||||||
|
|
||||||
|
|
||||||
|
class CompletionContext(object):
|
||||||
|
"""
|
||||||
|
Contains additional information about the context in which a completion request is triggered.
|
||||||
|
"""
|
||||||
|
def __init__(self, triggerKind, triggerCharacter = None):
|
||||||
|
"""
|
||||||
|
Constructs a new CompletionContext instance.
|
||||||
|
|
||||||
|
:param CompletionTriggerKind triggerKind: How the completion was triggered.
|
||||||
|
:param str triggerCharacter: The trigger character (a single character) that has trigger code complete.
|
||||||
|
Is undefined if `triggerKind !== CompletionTriggerKind.TriggerCharacter`
|
||||||
|
"""
|
||||||
|
self.triggerKind = triggerKind
|
||||||
|
if triggerCharacter:
|
||||||
|
self.triggerCharacter = triggerCharacter
|
||||||
|
|
||||||
|
|
||||||
|
class TextEdit(object):
|
||||||
|
"""
|
||||||
|
A textual edit applicable to a text document.
|
||||||
|
"""
|
||||||
|
def __init__(self, range, newText):
|
||||||
|
"""
|
||||||
|
:param Range range: The range of the text document to be manipulated. To insert
|
||||||
|
text into a document create a range where start === end.
|
||||||
|
:param str newText: The string to be inserted. For delete operations use an empty string.
|
||||||
|
"""
|
||||||
|
self.range = range
|
||||||
|
self.newText = newText
|
||||||
|
|
||||||
|
|
||||||
|
class InsertTextFormat(object):
|
||||||
|
PlainText = 1
|
||||||
|
Snippet = 2
|
||||||
|
|
||||||
|
|
||||||
|
class CompletionItem(object):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
def __init__(self, label, \
|
||||||
|
kind = None, \
|
||||||
|
detail = None, \
|
||||||
|
documentation = None, \
|
||||||
|
deprecated = None, \
|
||||||
|
preselect = None, \
|
||||||
|
sortText = None, \
|
||||||
|
filterText = None, \
|
||||||
|
insertText = None, \
|
||||||
|
insertTextFormat = None, \
|
||||||
|
textEdit = None, \
|
||||||
|
additionalTextEdits = None, \
|
||||||
|
commitCharacters = None, \
|
||||||
|
command = None, \
|
||||||
|
data = None, \
|
||||||
|
score = 0.0
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
:param str label: The label of this completion item. By default also the text that is inserted when selecting
|
||||||
|
this completion.
|
||||||
|
:param int kind: The kind of this completion item. Based of the kind an icon is chosen by the editor.
|
||||||
|
:param str detail: A human-readable string with additional information about this item, like type or symbol information.
|
||||||
|
:param tr ocumentation: A human-readable string that represents a doc-comment.
|
||||||
|
:param bool deprecated: Indicates if this item is deprecated.
|
||||||
|
:param bool preselect: Select this item when showing. Note: that only one completion item can be selected and that the
|
||||||
|
tool / client decides which item that is. The rule is that the first item of those that match best is selected.
|
||||||
|
:param str sortText: A string that should be used when comparing this item with other items. When `falsy` the label is used.
|
||||||
|
:param str filterText: A string that should be used when filtering a set of completion items. When `falsy` the label is used.
|
||||||
|
:param str insertText: A string that should be inserted into a document when selecting this completion. When `falsy` the label is used.
|
||||||
|
The `insertText` is subject to interpretation by the client side. Some tools might not take the string literally. For example
|
||||||
|
VS Code when code complete is requested in this example `con<cursor position>` and a completion item with an `insertText` of `console` is provided it
|
||||||
|
will only insert `sole`. Therefore it is recommended to use `textEdit` instead since it avoids additional client side interpretation.
|
||||||
|
@deprecated Use textEdit instead.
|
||||||
|
:param InsertTextFormat insertTextFormat: The format of the insert text. The format applies to both the `insertText` property
|
||||||
|
and the `newText` property of a provided `textEdit`.
|
||||||
|
:param TextEdit textEdit: An edit which is applied to a document when selecting this completion. When an edit is provided the value of `insertText` is ignored.
|
||||||
|
Note:* The range of the edit must be a single line range and it must contain the position at which completion
|
||||||
|
has been requested.
|
||||||
|
:param TextEdit additionalTextEdits: An optional array of additional text edits that are applied when selecting this completion.
|
||||||
|
Edits must not overlap (including the same insert position) with the main edit nor with themselves.
|
||||||
|
Additional text edits should be used to change text unrelated to the current cursor position
|
||||||
|
(for example adding an import statement at the top of the file if the completion item will
|
||||||
|
insert an unqualified type).
|
||||||
|
:param str commitCharacters: An optional set of characters that when pressed while this completion is active will accept it first and
|
||||||
|
then type that character. *Note* that all commit characters should have `length=1` and that superfluous
|
||||||
|
characters will be ignored.
|
||||||
|
:param Command command: An optional command that is executed *after* inserting this completion. Note: that
|
||||||
|
additional modifications to the current document should be described with the additionalTextEdits-property.
|
||||||
|
:param data: An data entry field that is preserved on a completion item between a completion and a completion resolve request.
|
||||||
|
:param float score: Score of the code completion item.
|
||||||
|
"""
|
||||||
|
self.label = label
|
||||||
|
self.kind = kind
|
||||||
|
self.detail = detail
|
||||||
|
self.documentation = documentation
|
||||||
|
self.deprecated = deprecated
|
||||||
|
self.preselect = preselect
|
||||||
|
self.sortText = sortText
|
||||||
|
self.filterText = filterText
|
||||||
|
self.insertText = insertText
|
||||||
|
self.insertTextFormat = insertTextFormat
|
||||||
|
self.textEdit = textEdit
|
||||||
|
self.additionalTextEdits = additionalTextEdits
|
||||||
|
self.commitCharacters = commitCharacters
|
||||||
|
self.command = command
|
||||||
|
self.data = data
|
||||||
|
self.score = score
|
||||||
|
|
||||||
|
|
||||||
|
class CompletionItemKind(enum.Enum):
|
||||||
|
Text = 1
|
||||||
|
Method = 2
|
||||||
|
Function = 3
|
||||||
|
Constructor = 4
|
||||||
|
Field = 5
|
||||||
|
Variable = 6
|
||||||
|
Class = 7
|
||||||
|
Interface = 8
|
||||||
|
Module = 9
|
||||||
|
Property = 10
|
||||||
|
Unit = 11
|
||||||
|
Value = 12
|
||||||
|
Enum = 13
|
||||||
|
Keyword = 14
|
||||||
|
Snippet = 15
|
||||||
|
Color = 16
|
||||||
|
File = 17
|
||||||
|
Reference = 18
|
||||||
|
Folder = 19
|
||||||
|
EnumMember = 20
|
||||||
|
Constant = 21
|
||||||
|
Struct = 22
|
||||||
|
Event = 23
|
||||||
|
Operator = 24
|
||||||
|
TypeParameter = 25
|
||||||
|
|
||||||
|
|
||||||
|
class CompletionList(object):
|
||||||
|
"""
|
||||||
|
Represents a collection of [completion items](#CompletionItem) to be preselect in the editor.
|
||||||
|
"""
|
||||||
|
def __init__(self, isIncomplete, items):
|
||||||
|
"""
|
||||||
|
Constructs a new CompletionContext instance.
|
||||||
|
|
||||||
|
:param bool isIncomplete: This list it not complete. Further typing should result in recomputing this list.
|
||||||
|
:param CompletionItem items: The completion items.
|
||||||
|
"""
|
||||||
|
self.isIncomplete = isIncomplete
|
||||||
|
self.items = [to_type(i, CompletionItem) for i in items]
|
||||||
|
|
||||||
|
class ErrorCodes(enum.Enum):
|
||||||
|
# Defined by JSON RPC
|
||||||
|
ParseError = -32700
|
||||||
|
InvalidRequest = -32600
|
||||||
|
MethodNotFound = -32601
|
||||||
|
InvalidParams = -32602
|
||||||
|
InternalError = -32603
|
||||||
|
serverErrorStart = -32099
|
||||||
|
serverErrorEnd = -32000
|
||||||
|
ServerNotInitialized = -32002
|
||||||
|
UnknownErrorCode = -32001
|
||||||
|
|
||||||
|
# Defined by the protocol.
|
||||||
|
RequestCancelled = -32800
|
||||||
|
ContentModified = -32801
|
||||||
|
|
||||||
|
class ResponseError(Exception):
|
||||||
|
def __init__(self, code, message, data = None):
|
||||||
|
self.code = code
|
||||||
|
self.message = message
|
||||||
|
if data:
|
||||||
|
self.data = data
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Pligin Libs Module
|
||||||
|
"""
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
"""
|
||||||
|
__init__.py
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright 2024 engn33r
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ._abnf import *
|
||||||
|
from ._app import WebSocketApp as WebSocketApp, setReconnect as setReconnect
|
||||||
|
from ._core import *
|
||||||
|
from ._exceptions import *
|
||||||
|
from ._logging import *
|
||||||
|
from ._socket import *
|
||||||
|
|
||||||
|
__version__ = "1.8.0"
|
||||||
@@ -0,0 +1,453 @@
|
|||||||
|
import array
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
from threading import Lock
|
||||||
|
from typing import Callable, Optional, Union
|
||||||
|
|
||||||
|
from ._exceptions import WebSocketPayloadException, WebSocketProtocolException
|
||||||
|
from ._utils import validate_utf8
|
||||||
|
|
||||||
|
"""
|
||||||
|
_abnf.py
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright 2024 engn33r
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
# If wsaccel is available, use compiled routines to mask data.
|
||||||
|
# wsaccel only provides around a 10% speed boost compared
|
||||||
|
# to the websocket-client _mask() implementation.
|
||||||
|
# Note that wsaccel is unmaintained.
|
||||||
|
from wsaccel.xormask import XorMaskerSimple
|
||||||
|
|
||||||
|
def _mask(mask_value: array.array, data_value: array.array) -> bytes:
|
||||||
|
mask_result: bytes = XorMaskerSimple(mask_value).process(data_value)
|
||||||
|
return mask_result
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
# wsaccel is not available, use websocket-client _mask()
|
||||||
|
native_byteorder = sys.byteorder
|
||||||
|
|
||||||
|
def _mask(mask_value: array.array, data_value: array.array) -> bytes:
|
||||||
|
datalen = len(data_value)
|
||||||
|
int_data_value = int.from_bytes(data_value, native_byteorder)
|
||||||
|
int_mask_value = int.from_bytes(
|
||||||
|
mask_value * (datalen // 4) + mask_value[: datalen % 4], native_byteorder
|
||||||
|
)
|
||||||
|
return (int_data_value ^ int_mask_value).to_bytes(datalen, native_byteorder)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"ABNF",
|
||||||
|
"continuous_frame",
|
||||||
|
"frame_buffer",
|
||||||
|
"STATUS_NORMAL",
|
||||||
|
"STATUS_GOING_AWAY",
|
||||||
|
"STATUS_PROTOCOL_ERROR",
|
||||||
|
"STATUS_UNSUPPORTED_DATA_TYPE",
|
||||||
|
"STATUS_STATUS_NOT_AVAILABLE",
|
||||||
|
"STATUS_ABNORMAL_CLOSED",
|
||||||
|
"STATUS_INVALID_PAYLOAD",
|
||||||
|
"STATUS_POLICY_VIOLATION",
|
||||||
|
"STATUS_MESSAGE_TOO_BIG",
|
||||||
|
"STATUS_INVALID_EXTENSION",
|
||||||
|
"STATUS_UNEXPECTED_CONDITION",
|
||||||
|
"STATUS_BAD_GATEWAY",
|
||||||
|
"STATUS_TLS_HANDSHAKE_ERROR",
|
||||||
|
]
|
||||||
|
|
||||||
|
# closing frame status codes.
|
||||||
|
STATUS_NORMAL = 1000
|
||||||
|
STATUS_GOING_AWAY = 1001
|
||||||
|
STATUS_PROTOCOL_ERROR = 1002
|
||||||
|
STATUS_UNSUPPORTED_DATA_TYPE = 1003
|
||||||
|
STATUS_STATUS_NOT_AVAILABLE = 1005
|
||||||
|
STATUS_ABNORMAL_CLOSED = 1006
|
||||||
|
STATUS_INVALID_PAYLOAD = 1007
|
||||||
|
STATUS_POLICY_VIOLATION = 1008
|
||||||
|
STATUS_MESSAGE_TOO_BIG = 1009
|
||||||
|
STATUS_INVALID_EXTENSION = 1010
|
||||||
|
STATUS_UNEXPECTED_CONDITION = 1011
|
||||||
|
STATUS_SERVICE_RESTART = 1012
|
||||||
|
STATUS_TRY_AGAIN_LATER = 1013
|
||||||
|
STATUS_BAD_GATEWAY = 1014
|
||||||
|
STATUS_TLS_HANDSHAKE_ERROR = 1015
|
||||||
|
|
||||||
|
VALID_CLOSE_STATUS = (
|
||||||
|
STATUS_NORMAL,
|
||||||
|
STATUS_GOING_AWAY,
|
||||||
|
STATUS_PROTOCOL_ERROR,
|
||||||
|
STATUS_UNSUPPORTED_DATA_TYPE,
|
||||||
|
STATUS_INVALID_PAYLOAD,
|
||||||
|
STATUS_POLICY_VIOLATION,
|
||||||
|
STATUS_MESSAGE_TOO_BIG,
|
||||||
|
STATUS_INVALID_EXTENSION,
|
||||||
|
STATUS_UNEXPECTED_CONDITION,
|
||||||
|
STATUS_SERVICE_RESTART,
|
||||||
|
STATUS_TRY_AGAIN_LATER,
|
||||||
|
STATUS_BAD_GATEWAY,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ABNF:
|
||||||
|
"""
|
||||||
|
ABNF frame class.
|
||||||
|
See http://tools.ietf.org/html/rfc5234
|
||||||
|
and http://tools.ietf.org/html/rfc6455#section-5.2
|
||||||
|
"""
|
||||||
|
|
||||||
|
# operation code values.
|
||||||
|
OPCODE_CONT = 0x0
|
||||||
|
OPCODE_TEXT = 0x1
|
||||||
|
OPCODE_BINARY = 0x2
|
||||||
|
OPCODE_CLOSE = 0x8
|
||||||
|
OPCODE_PING = 0x9
|
||||||
|
OPCODE_PONG = 0xA
|
||||||
|
|
||||||
|
# available operation code value tuple
|
||||||
|
OPCODES = (
|
||||||
|
OPCODE_CONT,
|
||||||
|
OPCODE_TEXT,
|
||||||
|
OPCODE_BINARY,
|
||||||
|
OPCODE_CLOSE,
|
||||||
|
OPCODE_PING,
|
||||||
|
OPCODE_PONG,
|
||||||
|
)
|
||||||
|
|
||||||
|
# opcode human readable string
|
||||||
|
OPCODE_MAP = {
|
||||||
|
OPCODE_CONT: "cont",
|
||||||
|
OPCODE_TEXT: "text",
|
||||||
|
OPCODE_BINARY: "binary",
|
||||||
|
OPCODE_CLOSE: "close",
|
||||||
|
OPCODE_PING: "ping",
|
||||||
|
OPCODE_PONG: "pong",
|
||||||
|
}
|
||||||
|
|
||||||
|
# data length threshold.
|
||||||
|
LENGTH_7 = 0x7E
|
||||||
|
LENGTH_16 = 1 << 16
|
||||||
|
LENGTH_63 = 1 << 63
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
fin: int = 0,
|
||||||
|
rsv1: int = 0,
|
||||||
|
rsv2: int = 0,
|
||||||
|
rsv3: int = 0,
|
||||||
|
opcode: int = OPCODE_TEXT,
|
||||||
|
mask_value: int = 1,
|
||||||
|
data: Union[str, bytes, None] = "",
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Constructor for ABNF. Please check RFC for arguments.
|
||||||
|
"""
|
||||||
|
self.fin = fin
|
||||||
|
self.rsv1 = rsv1
|
||||||
|
self.rsv2 = rsv2
|
||||||
|
self.rsv3 = rsv3
|
||||||
|
self.opcode = opcode
|
||||||
|
self.mask_value = mask_value
|
||||||
|
if data is None:
|
||||||
|
data = ""
|
||||||
|
self.data = data
|
||||||
|
self.get_mask_key = os.urandom
|
||||||
|
|
||||||
|
def validate(self, skip_utf8_validation: bool = False) -> None:
|
||||||
|
"""
|
||||||
|
Validate the ABNF frame.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
skip_utf8_validation: skip utf8 validation.
|
||||||
|
"""
|
||||||
|
if self.rsv1 or self.rsv2 or self.rsv3:
|
||||||
|
raise WebSocketProtocolException("rsv is not implemented, yet")
|
||||||
|
|
||||||
|
if self.opcode not in ABNF.OPCODES:
|
||||||
|
raise WebSocketProtocolException("Invalid opcode %r", self.opcode)
|
||||||
|
|
||||||
|
if self.opcode == ABNF.OPCODE_PING and not self.fin:
|
||||||
|
raise WebSocketProtocolException("Invalid ping frame.")
|
||||||
|
|
||||||
|
if self.opcode == ABNF.OPCODE_CLOSE:
|
||||||
|
l = len(self.data)
|
||||||
|
if not l:
|
||||||
|
return
|
||||||
|
if l == 1 or l >= 126:
|
||||||
|
raise WebSocketProtocolException("Invalid close frame.")
|
||||||
|
if l > 2 and not skip_utf8_validation and not validate_utf8(self.data[2:]):
|
||||||
|
raise WebSocketProtocolException("Invalid close frame.")
|
||||||
|
|
||||||
|
code = 256 * int(self.data[0]) + int(self.data[1])
|
||||||
|
if not self._is_valid_close_status(code):
|
||||||
|
raise WebSocketProtocolException("Invalid close opcode %r", code)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_valid_close_status(code: int) -> bool:
|
||||||
|
return code in VALID_CLOSE_STATUS or (3000 <= code < 5000)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"fin={self.fin} opcode={self.opcode} data={self.data}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_frame(data: Union[bytes, str], opcode: int, fin: int = 1) -> "ABNF":
|
||||||
|
"""
|
||||||
|
Create frame to send text, binary and other data.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
data: str
|
||||||
|
data to send. This is string value(byte array).
|
||||||
|
If opcode is OPCODE_TEXT and this value is unicode,
|
||||||
|
data value is converted into unicode string, automatically.
|
||||||
|
opcode: int
|
||||||
|
operation code. please see OPCODE_MAP.
|
||||||
|
fin: int
|
||||||
|
fin flag. if set to 0, create continue fragmentation.
|
||||||
|
"""
|
||||||
|
if opcode == ABNF.OPCODE_TEXT and isinstance(data, str):
|
||||||
|
data = data.encode("utf-8")
|
||||||
|
# mask must be set if send data from client
|
||||||
|
return ABNF(fin, 0, 0, 0, opcode, 1, data)
|
||||||
|
|
||||||
|
def format(self) -> bytes:
|
||||||
|
"""
|
||||||
|
Format this object to string(byte array) to send data to server.
|
||||||
|
"""
|
||||||
|
if any(x not in (0, 1) for x in [self.fin, self.rsv1, self.rsv2, self.rsv3]):
|
||||||
|
raise ValueError("not 0 or 1")
|
||||||
|
if self.opcode not in ABNF.OPCODES:
|
||||||
|
raise ValueError("Invalid OPCODE")
|
||||||
|
length = len(self.data)
|
||||||
|
if length >= ABNF.LENGTH_63:
|
||||||
|
raise ValueError("data is too long")
|
||||||
|
|
||||||
|
frame_header = chr(
|
||||||
|
self.fin << 7
|
||||||
|
| self.rsv1 << 6
|
||||||
|
| self.rsv2 << 5
|
||||||
|
| self.rsv3 << 4
|
||||||
|
| self.opcode
|
||||||
|
).encode("latin-1")
|
||||||
|
if length < ABNF.LENGTH_7:
|
||||||
|
frame_header += chr(self.mask_value << 7 | length).encode("latin-1")
|
||||||
|
elif length < ABNF.LENGTH_16:
|
||||||
|
frame_header += chr(self.mask_value << 7 | 0x7E).encode("latin-1")
|
||||||
|
frame_header += struct.pack("!H", length)
|
||||||
|
else:
|
||||||
|
frame_header += chr(self.mask_value << 7 | 0x7F).encode("latin-1")
|
||||||
|
frame_header += struct.pack("!Q", length)
|
||||||
|
|
||||||
|
if not self.mask_value:
|
||||||
|
if isinstance(self.data, str):
|
||||||
|
self.data = self.data.encode("utf-8")
|
||||||
|
return frame_header + self.data
|
||||||
|
mask_key = self.get_mask_key(4)
|
||||||
|
return frame_header + self._get_masked(mask_key)
|
||||||
|
|
||||||
|
def _get_masked(self, mask_key: Union[str, bytes]) -> bytes:
|
||||||
|
s = ABNF.mask(mask_key, self.data)
|
||||||
|
|
||||||
|
if isinstance(mask_key, str):
|
||||||
|
mask_key = mask_key.encode("utf-8")
|
||||||
|
|
||||||
|
return mask_key + s
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def mask(mask_key: Union[str, bytes], data: Union[str, bytes]) -> bytes:
|
||||||
|
"""
|
||||||
|
Mask or unmask data. Just do xor for each byte
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
mask_key: bytes or str
|
||||||
|
4 byte mask.
|
||||||
|
data: bytes or str
|
||||||
|
data to mask/unmask.
|
||||||
|
"""
|
||||||
|
if data is None:
|
||||||
|
data = ""
|
||||||
|
|
||||||
|
if isinstance(mask_key, str):
|
||||||
|
mask_key = mask_key.encode("latin-1")
|
||||||
|
|
||||||
|
if isinstance(data, str):
|
||||||
|
data = data.encode("latin-1")
|
||||||
|
|
||||||
|
return _mask(array.array("B", mask_key), array.array("B", data))
|
||||||
|
|
||||||
|
|
||||||
|
class frame_buffer:
|
||||||
|
_HEADER_MASK_INDEX = 5
|
||||||
|
_HEADER_LENGTH_INDEX = 6
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, recv_fn: Callable[[int], int], skip_utf8_validation: bool
|
||||||
|
) -> None:
|
||||||
|
self.recv = recv_fn
|
||||||
|
self.skip_utf8_validation = skip_utf8_validation
|
||||||
|
# Buffers over the packets from the layer beneath until desired amount
|
||||||
|
# bytes of bytes are received.
|
||||||
|
self.recv_buffer: list = []
|
||||||
|
self.clear()
|
||||||
|
self.lock = Lock()
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
self.header: Optional[tuple] = None
|
||||||
|
self.length: Optional[int] = None
|
||||||
|
self.mask_value: Union[bytes, str, None] = None
|
||||||
|
|
||||||
|
def has_received_header(self) -> bool:
|
||||||
|
return self.header is None
|
||||||
|
|
||||||
|
def recv_header(self) -> None:
|
||||||
|
header = self.recv_strict(2)
|
||||||
|
b1 = header[0]
|
||||||
|
fin = b1 >> 7 & 1
|
||||||
|
rsv1 = b1 >> 6 & 1
|
||||||
|
rsv2 = b1 >> 5 & 1
|
||||||
|
rsv3 = b1 >> 4 & 1
|
||||||
|
opcode = b1 & 0xF
|
||||||
|
b2 = header[1]
|
||||||
|
has_mask = b2 >> 7 & 1
|
||||||
|
length_bits = b2 & 0x7F
|
||||||
|
|
||||||
|
self.header = (fin, rsv1, rsv2, rsv3, opcode, has_mask, length_bits)
|
||||||
|
|
||||||
|
def has_mask(self) -> Union[bool, int]:
|
||||||
|
if not self.header:
|
||||||
|
return False
|
||||||
|
header_val: int = self.header[frame_buffer._HEADER_MASK_INDEX]
|
||||||
|
return header_val
|
||||||
|
|
||||||
|
def has_received_length(self) -> bool:
|
||||||
|
return self.length is None
|
||||||
|
|
||||||
|
def recv_length(self) -> None:
|
||||||
|
bits = self.header[frame_buffer._HEADER_LENGTH_INDEX]
|
||||||
|
length_bits = bits & 0x7F
|
||||||
|
if length_bits == 0x7E:
|
||||||
|
v = self.recv_strict(2)
|
||||||
|
self.length = struct.unpack("!H", v)[0]
|
||||||
|
elif length_bits == 0x7F:
|
||||||
|
v = self.recv_strict(8)
|
||||||
|
self.length = struct.unpack("!Q", v)[0]
|
||||||
|
else:
|
||||||
|
self.length = length_bits
|
||||||
|
|
||||||
|
def has_received_mask(self) -> bool:
|
||||||
|
return self.mask_value is None
|
||||||
|
|
||||||
|
def recv_mask(self) -> None:
|
||||||
|
self.mask_value = self.recv_strict(4) if self.has_mask() else ""
|
||||||
|
|
||||||
|
def recv_frame(self) -> ABNF:
|
||||||
|
with self.lock:
|
||||||
|
# Header
|
||||||
|
if self.has_received_header():
|
||||||
|
self.recv_header()
|
||||||
|
(fin, rsv1, rsv2, rsv3, opcode, has_mask, _) = self.header
|
||||||
|
|
||||||
|
# Frame length
|
||||||
|
if self.has_received_length():
|
||||||
|
self.recv_length()
|
||||||
|
length = self.length
|
||||||
|
|
||||||
|
# Mask
|
||||||
|
if self.has_received_mask():
|
||||||
|
self.recv_mask()
|
||||||
|
mask_value = self.mask_value
|
||||||
|
|
||||||
|
# Payload
|
||||||
|
payload = self.recv_strict(length)
|
||||||
|
if has_mask:
|
||||||
|
payload = ABNF.mask(mask_value, payload)
|
||||||
|
|
||||||
|
# Reset for next frame
|
||||||
|
self.clear()
|
||||||
|
|
||||||
|
frame = ABNF(fin, rsv1, rsv2, rsv3, opcode, has_mask, payload)
|
||||||
|
frame.validate(self.skip_utf8_validation)
|
||||||
|
|
||||||
|
return frame
|
||||||
|
|
||||||
|
def recv_strict(self, bufsize: int) -> bytes:
|
||||||
|
shortage = bufsize - sum(map(len, self.recv_buffer))
|
||||||
|
while shortage > 0:
|
||||||
|
# Limit buffer size that we pass to socket.recv() to avoid
|
||||||
|
# fragmenting the heap -- the number of bytes recv() actually
|
||||||
|
# reads is limited by socket buffer and is relatively small,
|
||||||
|
# yet passing large numbers repeatedly causes lots of large
|
||||||
|
# buffers allocated and then shrunk, which results in
|
||||||
|
# fragmentation.
|
||||||
|
bytes_ = self.recv(min(16384, shortage))
|
||||||
|
self.recv_buffer.append(bytes_)
|
||||||
|
shortage -= len(bytes_)
|
||||||
|
|
||||||
|
unified = b"".join(self.recv_buffer)
|
||||||
|
|
||||||
|
if shortage == 0:
|
||||||
|
self.recv_buffer = []
|
||||||
|
return unified
|
||||||
|
else:
|
||||||
|
self.recv_buffer = [unified[bufsize:]]
|
||||||
|
return unified[:bufsize]
|
||||||
|
|
||||||
|
|
||||||
|
class continuous_frame:
|
||||||
|
def __init__(self, fire_cont_frame: bool, skip_utf8_validation: bool) -> None:
|
||||||
|
self.fire_cont_frame = fire_cont_frame
|
||||||
|
self.skip_utf8_validation = skip_utf8_validation
|
||||||
|
self.cont_data: Optional[list] = None
|
||||||
|
self.recving_frames: Optional[int] = None
|
||||||
|
|
||||||
|
def validate(self, frame: ABNF) -> None:
|
||||||
|
if not self.recving_frames and frame.opcode == ABNF.OPCODE_CONT:
|
||||||
|
raise WebSocketProtocolException("Illegal frame")
|
||||||
|
if self.recving_frames and frame.opcode in (
|
||||||
|
ABNF.OPCODE_TEXT,
|
||||||
|
ABNF.OPCODE_BINARY,
|
||||||
|
):
|
||||||
|
raise WebSocketProtocolException("Illegal frame")
|
||||||
|
|
||||||
|
def add(self, frame: ABNF) -> None:
|
||||||
|
if self.cont_data:
|
||||||
|
self.cont_data[1] += frame.data
|
||||||
|
else:
|
||||||
|
if frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY):
|
||||||
|
self.recving_frames = frame.opcode
|
||||||
|
self.cont_data = [frame.opcode, frame.data]
|
||||||
|
|
||||||
|
if frame.fin:
|
||||||
|
self.recving_frames = None
|
||||||
|
|
||||||
|
def is_fire(self, frame: ABNF) -> Union[bool, int]:
|
||||||
|
return frame.fin or self.fire_cont_frame
|
||||||
|
|
||||||
|
def extract(self, frame: ABNF) -> tuple:
|
||||||
|
data = self.cont_data
|
||||||
|
self.cont_data = None
|
||||||
|
frame.data = data[1]
|
||||||
|
if (
|
||||||
|
not self.fire_cont_frame
|
||||||
|
and data[0] == ABNF.OPCODE_TEXT
|
||||||
|
and not self.skip_utf8_validation
|
||||||
|
and not validate_utf8(frame.data)
|
||||||
|
):
|
||||||
|
raise WebSocketPayloadException(f"cannot decode: {repr(frame.data)}")
|
||||||
|
return data[0], frame
|
||||||
@@ -0,0 +1,677 @@
|
|||||||
|
import inspect
|
||||||
|
import selectors
|
||||||
|
import socket
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from typing import Any, Callable, Optional, Union
|
||||||
|
|
||||||
|
from . import _logging
|
||||||
|
from ._abnf import ABNF
|
||||||
|
from ._core import WebSocket, getdefaulttimeout
|
||||||
|
from ._exceptions import (
|
||||||
|
WebSocketConnectionClosedException,
|
||||||
|
WebSocketException,
|
||||||
|
WebSocketTimeoutException,
|
||||||
|
)
|
||||||
|
from ._ssl_compat import SSLEOFError
|
||||||
|
from ._url import parse_url
|
||||||
|
|
||||||
|
"""
|
||||||
|
_app.py
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright 2024 engn33r
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__all__ = ["WebSocketApp"]
|
||||||
|
|
||||||
|
RECONNECT = 0
|
||||||
|
|
||||||
|
|
||||||
|
def setReconnect(reconnectInterval: int) -> None:
|
||||||
|
global RECONNECT
|
||||||
|
RECONNECT = reconnectInterval
|
||||||
|
|
||||||
|
|
||||||
|
class DispatcherBase:
|
||||||
|
"""
|
||||||
|
DispatcherBase
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, app: Any, ping_timeout: Union[float, int, None]) -> None:
|
||||||
|
self.app = app
|
||||||
|
self.ping_timeout = ping_timeout
|
||||||
|
|
||||||
|
def timeout(self, seconds: Union[float, int, None], callback: Callable) -> None:
|
||||||
|
time.sleep(seconds)
|
||||||
|
callback()
|
||||||
|
|
||||||
|
def reconnect(self, seconds: int, reconnector: Callable) -> None:
|
||||||
|
try:
|
||||||
|
_logging.info(
|
||||||
|
f"reconnect() - retrying in {seconds} seconds [{len(inspect.stack())} frames in stack]"
|
||||||
|
)
|
||||||
|
time.sleep(seconds)
|
||||||
|
reconnector(reconnecting=True)
|
||||||
|
except KeyboardInterrupt as e:
|
||||||
|
_logging.info(f"User exited {e}")
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
class Dispatcher(DispatcherBase):
|
||||||
|
"""
|
||||||
|
Dispatcher
|
||||||
|
"""
|
||||||
|
|
||||||
|
def read(
|
||||||
|
self,
|
||||||
|
sock: socket.socket,
|
||||||
|
read_callback: Callable,
|
||||||
|
check_callback: Callable,
|
||||||
|
) -> None:
|
||||||
|
sel = selectors.DefaultSelector()
|
||||||
|
sel.register(self.app.sock.sock, selectors.EVENT_READ)
|
||||||
|
try:
|
||||||
|
while self.app.keep_running:
|
||||||
|
if sel.select(self.ping_timeout):
|
||||||
|
if not read_callback():
|
||||||
|
break
|
||||||
|
check_callback()
|
||||||
|
finally:
|
||||||
|
sel.close()
|
||||||
|
|
||||||
|
|
||||||
|
class SSLDispatcher(DispatcherBase):
|
||||||
|
"""
|
||||||
|
SSLDispatcher
|
||||||
|
"""
|
||||||
|
|
||||||
|
def read(
|
||||||
|
self,
|
||||||
|
sock: socket.socket,
|
||||||
|
read_callback: Callable,
|
||||||
|
check_callback: Callable,
|
||||||
|
) -> None:
|
||||||
|
sock = self.app.sock.sock
|
||||||
|
sel = selectors.DefaultSelector()
|
||||||
|
sel.register(sock, selectors.EVENT_READ)
|
||||||
|
try:
|
||||||
|
while self.app.keep_running:
|
||||||
|
if self.select(sock, sel):
|
||||||
|
if not read_callback():
|
||||||
|
break
|
||||||
|
check_callback()
|
||||||
|
finally:
|
||||||
|
sel.close()
|
||||||
|
|
||||||
|
def select(self, sock, sel: selectors.DefaultSelector):
|
||||||
|
sock = self.app.sock.sock
|
||||||
|
if sock.pending():
|
||||||
|
return [
|
||||||
|
sock,
|
||||||
|
]
|
||||||
|
|
||||||
|
r = sel.select(self.ping_timeout)
|
||||||
|
|
||||||
|
if len(r) > 0:
|
||||||
|
return r[0][0]
|
||||||
|
|
||||||
|
|
||||||
|
class WrappedDispatcher:
|
||||||
|
"""
|
||||||
|
WrappedDispatcher
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, app, ping_timeout: Union[float, int, None], dispatcher) -> None:
|
||||||
|
self.app = app
|
||||||
|
self.ping_timeout = ping_timeout
|
||||||
|
self.dispatcher = dispatcher
|
||||||
|
dispatcher.signal(2, dispatcher.abort) # keyboard interrupt
|
||||||
|
|
||||||
|
def read(
|
||||||
|
self,
|
||||||
|
sock: socket.socket,
|
||||||
|
read_callback: Callable,
|
||||||
|
check_callback: Callable,
|
||||||
|
) -> None:
|
||||||
|
self.dispatcher.read(sock, read_callback)
|
||||||
|
self.ping_timeout and self.timeout(self.ping_timeout, check_callback)
|
||||||
|
|
||||||
|
def timeout(self, seconds: float, callback: Callable) -> None:
|
||||||
|
self.dispatcher.timeout(seconds, callback)
|
||||||
|
|
||||||
|
def reconnect(self, seconds: int, reconnector: Callable) -> None:
|
||||||
|
self.timeout(seconds, reconnector)
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketApp:
|
||||||
|
"""
|
||||||
|
Higher level of APIs are provided. The interface is like JavaScript WebSocket object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
url: str,
|
||||||
|
header: Union[list, dict, Callable, None] = None,
|
||||||
|
on_open: Optional[Callable[[WebSocket], None]] = None,
|
||||||
|
on_reconnect: Optional[Callable[[WebSocket], None]] = None,
|
||||||
|
on_message: Optional[Callable[[WebSocket, Any], None]] = None,
|
||||||
|
on_error: Optional[Callable[[WebSocket, Any], None]] = None,
|
||||||
|
on_close: Optional[Callable[[WebSocket, Any, Any], None]] = None,
|
||||||
|
on_ping: Optional[Callable] = None,
|
||||||
|
on_pong: Optional[Callable] = None,
|
||||||
|
on_cont_message: Optional[Callable] = None,
|
||||||
|
keep_running: bool = True,
|
||||||
|
get_mask_key: Optional[Callable] = None,
|
||||||
|
cookie: Optional[str] = None,
|
||||||
|
subprotocols: Optional[list] = None,
|
||||||
|
on_data: Optional[Callable] = None,
|
||||||
|
socket: Optional[socket.socket] = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
WebSocketApp initialization
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
url: str
|
||||||
|
Websocket url.
|
||||||
|
header: list or dict or Callable
|
||||||
|
Custom header for websocket handshake.
|
||||||
|
If the parameter is a callable object, it is called just before the connection attempt.
|
||||||
|
The returned dict or list is used as custom header value.
|
||||||
|
This could be useful in order to properly setup timestamp dependent headers.
|
||||||
|
on_open: function
|
||||||
|
Callback object which is called at opening websocket.
|
||||||
|
on_open has one argument.
|
||||||
|
The 1st argument is this class object.
|
||||||
|
on_reconnect: function
|
||||||
|
Callback object which is called at reconnecting websocket.
|
||||||
|
on_reconnect has one argument.
|
||||||
|
The 1st argument is this class object.
|
||||||
|
on_message: function
|
||||||
|
Callback object which is called when received data.
|
||||||
|
on_message has 2 arguments.
|
||||||
|
The 1st argument is this class object.
|
||||||
|
The 2nd argument is utf-8 data received from the server.
|
||||||
|
on_error: function
|
||||||
|
Callback object which is called when we get error.
|
||||||
|
on_error has 2 arguments.
|
||||||
|
The 1st argument is this class object.
|
||||||
|
The 2nd argument is exception object.
|
||||||
|
on_close: function
|
||||||
|
Callback object which is called when connection is closed.
|
||||||
|
on_close has 3 arguments.
|
||||||
|
The 1st argument is this class object.
|
||||||
|
The 2nd argument is close_status_code.
|
||||||
|
The 3rd argument is close_msg.
|
||||||
|
on_cont_message: function
|
||||||
|
Callback object which is called when a continuation
|
||||||
|
frame is received.
|
||||||
|
on_cont_message has 3 arguments.
|
||||||
|
The 1st argument is this class object.
|
||||||
|
The 2nd argument is utf-8 string which we get from the server.
|
||||||
|
The 3rd argument is continue flag. if 0, the data continue
|
||||||
|
to next frame data
|
||||||
|
on_data: function
|
||||||
|
Callback object which is called when a message received.
|
||||||
|
This is called before on_message or on_cont_message,
|
||||||
|
and then on_message or on_cont_message is called.
|
||||||
|
on_data has 4 argument.
|
||||||
|
The 1st argument is this class object.
|
||||||
|
The 2nd argument is utf-8 string which we get from the server.
|
||||||
|
The 3rd argument is data type. ABNF.OPCODE_TEXT or ABNF.OPCODE_BINARY will be came.
|
||||||
|
The 4th argument is continue flag. If 0, the data continue
|
||||||
|
keep_running: bool
|
||||||
|
This parameter is obsolete and ignored.
|
||||||
|
get_mask_key: function
|
||||||
|
A callable function to get new mask keys, see the
|
||||||
|
WebSocket.set_mask_key's docstring for more information.
|
||||||
|
cookie: str
|
||||||
|
Cookie value.
|
||||||
|
subprotocols: list
|
||||||
|
List of available sub protocols. Default is None.
|
||||||
|
socket: socket
|
||||||
|
Pre-initialized stream socket.
|
||||||
|
"""
|
||||||
|
self.url = url
|
||||||
|
self.header = header if header is not None else []
|
||||||
|
self.cookie = cookie
|
||||||
|
|
||||||
|
self.on_open = on_open
|
||||||
|
self.on_reconnect = on_reconnect
|
||||||
|
self.on_message = on_message
|
||||||
|
self.on_data = on_data
|
||||||
|
self.on_error = on_error
|
||||||
|
self.on_close = on_close
|
||||||
|
self.on_ping = on_ping
|
||||||
|
self.on_pong = on_pong
|
||||||
|
self.on_cont_message = on_cont_message
|
||||||
|
self.keep_running = False
|
||||||
|
self.get_mask_key = get_mask_key
|
||||||
|
self.sock: Optional[WebSocket] = None
|
||||||
|
self.last_ping_tm = float(0)
|
||||||
|
self.last_pong_tm = float(0)
|
||||||
|
self.ping_thread: Optional[threading.Thread] = None
|
||||||
|
self.stop_ping: Optional[threading.Event] = None
|
||||||
|
self.ping_interval = float(0)
|
||||||
|
self.ping_timeout: Union[float, int, None] = None
|
||||||
|
self.ping_payload = ""
|
||||||
|
self.subprotocols = subprotocols
|
||||||
|
self.prepared_socket = socket
|
||||||
|
self.has_errored = False
|
||||||
|
self.has_done_teardown = False
|
||||||
|
self.has_done_teardown_lock = threading.Lock()
|
||||||
|
|
||||||
|
def send(self, data: Union[bytes, str], opcode: int = ABNF.OPCODE_TEXT) -> None:
|
||||||
|
"""
|
||||||
|
send message
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
data: str
|
||||||
|
Message to send. If you set opcode to OPCODE_TEXT,
|
||||||
|
data must be utf-8 string or unicode.
|
||||||
|
opcode: int
|
||||||
|
Operation code of data. Default is OPCODE_TEXT.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self.sock or self.sock.send(data, opcode) == 0:
|
||||||
|
raise WebSocketConnectionClosedException("Connection is already closed.")
|
||||||
|
|
||||||
|
def send_text(self, text_data: str) -> None:
|
||||||
|
"""
|
||||||
|
Sends UTF-8 encoded text.
|
||||||
|
"""
|
||||||
|
if not self.sock or self.sock.send(text_data, ABNF.OPCODE_TEXT) == 0:
|
||||||
|
raise WebSocketConnectionClosedException("Connection is already closed.")
|
||||||
|
|
||||||
|
def send_bytes(self, data: Union[bytes, bytearray]) -> None:
|
||||||
|
"""
|
||||||
|
Sends a sequence of bytes.
|
||||||
|
"""
|
||||||
|
if not self.sock or self.sock.send(data, ABNF.OPCODE_BINARY) == 0:
|
||||||
|
raise WebSocketConnectionClosedException("Connection is already closed.")
|
||||||
|
|
||||||
|
def close(self, **kwargs) -> None:
|
||||||
|
"""
|
||||||
|
Close websocket connection.
|
||||||
|
"""
|
||||||
|
self.keep_running = False
|
||||||
|
if self.sock:
|
||||||
|
self.sock.close(**kwargs)
|
||||||
|
self.sock = None
|
||||||
|
|
||||||
|
def _start_ping_thread(self) -> None:
|
||||||
|
self.last_ping_tm = self.last_pong_tm = float(0)
|
||||||
|
self.stop_ping = threading.Event()
|
||||||
|
self.ping_thread = threading.Thread(target=self._send_ping)
|
||||||
|
self.ping_thread.daemon = True
|
||||||
|
self.ping_thread.start()
|
||||||
|
|
||||||
|
def _stop_ping_thread(self) -> None:
|
||||||
|
if self.stop_ping:
|
||||||
|
self.stop_ping.set()
|
||||||
|
if self.ping_thread and self.ping_thread.is_alive():
|
||||||
|
self.ping_thread.join(3)
|
||||||
|
self.last_ping_tm = self.last_pong_tm = float(0)
|
||||||
|
|
||||||
|
def _send_ping(self) -> None:
|
||||||
|
if self.stop_ping.wait(self.ping_interval) or self.keep_running is False:
|
||||||
|
return
|
||||||
|
while not self.stop_ping.wait(self.ping_interval) and self.keep_running is True:
|
||||||
|
if self.sock:
|
||||||
|
self.last_ping_tm = time.time()
|
||||||
|
try:
|
||||||
|
_logging.debug("Sending ping")
|
||||||
|
self.sock.ping(self.ping_payload)
|
||||||
|
except Exception as e:
|
||||||
|
_logging.debug(f"Failed to send ping: {e}")
|
||||||
|
|
||||||
|
def run_forever(
|
||||||
|
self,
|
||||||
|
sockopt: tuple = None,
|
||||||
|
sslopt: dict = None,
|
||||||
|
ping_interval: Union[float, int] = 0,
|
||||||
|
ping_timeout: Union[float, int, None] = None,
|
||||||
|
ping_payload: str = "",
|
||||||
|
http_proxy_host: str = None,
|
||||||
|
http_proxy_port: Union[int, str] = None,
|
||||||
|
http_no_proxy: list = None,
|
||||||
|
http_proxy_auth: tuple = None,
|
||||||
|
http_proxy_timeout: Optional[float] = None,
|
||||||
|
skip_utf8_validation: bool = False,
|
||||||
|
host: str = None,
|
||||||
|
origin: str = None,
|
||||||
|
dispatcher=None,
|
||||||
|
suppress_origin: bool = False,
|
||||||
|
proxy_type: str = None,
|
||||||
|
reconnect: int = None,
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Run event loop for WebSocket framework.
|
||||||
|
|
||||||
|
This loop is an infinite loop and is alive while websocket is available.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
sockopt: tuple
|
||||||
|
Values for socket.setsockopt.
|
||||||
|
sockopt must be tuple
|
||||||
|
and each element is argument of sock.setsockopt.
|
||||||
|
sslopt: dict
|
||||||
|
Optional dict object for ssl socket option.
|
||||||
|
ping_interval: int or float
|
||||||
|
Automatically send "ping" command
|
||||||
|
every specified period (in seconds).
|
||||||
|
If set to 0, no ping is sent periodically.
|
||||||
|
ping_timeout: int or float
|
||||||
|
Timeout (in seconds) if the pong message is not received.
|
||||||
|
ping_payload: str
|
||||||
|
Payload message to send with each ping.
|
||||||
|
http_proxy_host: str
|
||||||
|
HTTP proxy host name.
|
||||||
|
http_proxy_port: int or str
|
||||||
|
HTTP proxy port. If not set, set to 80.
|
||||||
|
http_no_proxy: list
|
||||||
|
Whitelisted host names that don't use the proxy.
|
||||||
|
http_proxy_timeout: int or float
|
||||||
|
HTTP proxy timeout, default is 60 sec as per python-socks.
|
||||||
|
http_proxy_auth: tuple
|
||||||
|
HTTP proxy auth information. tuple of username and password. Default is None.
|
||||||
|
skip_utf8_validation: bool
|
||||||
|
skip utf8 validation.
|
||||||
|
host: str
|
||||||
|
update host header.
|
||||||
|
origin: str
|
||||||
|
update origin header.
|
||||||
|
dispatcher: Dispatcher object
|
||||||
|
customize reading data from socket.
|
||||||
|
suppress_origin: bool
|
||||||
|
suppress outputting origin header.
|
||||||
|
proxy_type: str
|
||||||
|
type of proxy from: http, socks4, socks4a, socks5, socks5h
|
||||||
|
reconnect: int
|
||||||
|
delay interval when reconnecting
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
teardown: bool
|
||||||
|
False if the `WebSocketApp` is closed or caught KeyboardInterrupt,
|
||||||
|
True if any other exception was raised during a loop.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if reconnect is None:
|
||||||
|
reconnect = RECONNECT
|
||||||
|
|
||||||
|
if ping_timeout is not None and ping_timeout <= 0:
|
||||||
|
raise WebSocketException("Ensure ping_timeout > 0")
|
||||||
|
if ping_interval is not None and ping_interval < 0:
|
||||||
|
raise WebSocketException("Ensure ping_interval >= 0")
|
||||||
|
if ping_timeout and ping_interval and ping_interval <= ping_timeout:
|
||||||
|
raise WebSocketException("Ensure ping_interval > ping_timeout")
|
||||||
|
if not sockopt:
|
||||||
|
sockopt = ()
|
||||||
|
if not sslopt:
|
||||||
|
sslopt = {}
|
||||||
|
if self.sock:
|
||||||
|
raise WebSocketException("socket is already opened")
|
||||||
|
|
||||||
|
self.ping_interval = ping_interval
|
||||||
|
self.ping_timeout = ping_timeout
|
||||||
|
self.ping_payload = ping_payload
|
||||||
|
self.has_done_teardown = False
|
||||||
|
self.keep_running = True
|
||||||
|
|
||||||
|
def teardown(close_frame: ABNF = None):
|
||||||
|
"""
|
||||||
|
Tears down the connection.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
close_frame: ABNF frame
|
||||||
|
If close_frame is set, the on_close handler is invoked
|
||||||
|
with the statusCode and reason from the provided frame.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# teardown() is called in many code paths to ensure resources are cleaned up and on_close is fired.
|
||||||
|
# To ensure the work is only done once, we use this bool and lock.
|
||||||
|
with self.has_done_teardown_lock:
|
||||||
|
if self.has_done_teardown:
|
||||||
|
return
|
||||||
|
self.has_done_teardown = True
|
||||||
|
|
||||||
|
self._stop_ping_thread()
|
||||||
|
self.keep_running = False
|
||||||
|
if self.sock:
|
||||||
|
self.sock.close()
|
||||||
|
close_status_code, close_reason = self._get_close_args(
|
||||||
|
close_frame if close_frame else None
|
||||||
|
)
|
||||||
|
self.sock = None
|
||||||
|
|
||||||
|
# Finally call the callback AFTER all teardown is complete
|
||||||
|
self._callback(self.on_close, close_status_code, close_reason)
|
||||||
|
|
||||||
|
def setSock(reconnecting: bool = False) -> None:
|
||||||
|
if reconnecting and self.sock:
|
||||||
|
self.sock.shutdown()
|
||||||
|
|
||||||
|
self.sock = WebSocket(
|
||||||
|
self.get_mask_key,
|
||||||
|
sockopt=sockopt,
|
||||||
|
sslopt=sslopt,
|
||||||
|
fire_cont_frame=self.on_cont_message is not None,
|
||||||
|
skip_utf8_validation=skip_utf8_validation,
|
||||||
|
enable_multithread=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.sock.settimeout(getdefaulttimeout())
|
||||||
|
try:
|
||||||
|
header = self.header() if callable(self.header) else self.header
|
||||||
|
|
||||||
|
self.sock.connect(
|
||||||
|
self.url,
|
||||||
|
header=header,
|
||||||
|
cookie=self.cookie,
|
||||||
|
http_proxy_host=http_proxy_host,
|
||||||
|
http_proxy_port=http_proxy_port,
|
||||||
|
http_no_proxy=http_no_proxy,
|
||||||
|
http_proxy_auth=http_proxy_auth,
|
||||||
|
http_proxy_timeout=http_proxy_timeout,
|
||||||
|
subprotocols=self.subprotocols,
|
||||||
|
host=host,
|
||||||
|
origin=origin,
|
||||||
|
suppress_origin=suppress_origin,
|
||||||
|
proxy_type=proxy_type,
|
||||||
|
socket=self.prepared_socket,
|
||||||
|
)
|
||||||
|
|
||||||
|
_logging.info("Websocket connected")
|
||||||
|
|
||||||
|
if self.ping_interval:
|
||||||
|
self._start_ping_thread()
|
||||||
|
|
||||||
|
if reconnecting and self.on_reconnect:
|
||||||
|
self._callback(self.on_reconnect)
|
||||||
|
else:
|
||||||
|
self._callback(self.on_open)
|
||||||
|
|
||||||
|
dispatcher.read(self.sock.sock, read, check)
|
||||||
|
except (
|
||||||
|
WebSocketConnectionClosedException,
|
||||||
|
ConnectionRefusedError,
|
||||||
|
KeyboardInterrupt,
|
||||||
|
SystemExit,
|
||||||
|
Exception,
|
||||||
|
) as e:
|
||||||
|
handleDisconnect(e, reconnecting)
|
||||||
|
|
||||||
|
def read() -> bool:
|
||||||
|
if not self.keep_running:
|
||||||
|
return teardown()
|
||||||
|
|
||||||
|
try:
|
||||||
|
op_code, frame = self.sock.recv_data_frame(True)
|
||||||
|
except (
|
||||||
|
WebSocketConnectionClosedException,
|
||||||
|
KeyboardInterrupt,
|
||||||
|
SSLEOFError,
|
||||||
|
) as e:
|
||||||
|
if custom_dispatcher:
|
||||||
|
return handleDisconnect(e, bool(reconnect))
|
||||||
|
else:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
if op_code == ABNF.OPCODE_CLOSE:
|
||||||
|
return teardown(frame)
|
||||||
|
elif op_code == ABNF.OPCODE_PING:
|
||||||
|
self._callback(self.on_ping, frame.data)
|
||||||
|
elif op_code == ABNF.OPCODE_PONG:
|
||||||
|
self.last_pong_tm = time.time()
|
||||||
|
self._callback(self.on_pong, frame.data)
|
||||||
|
elif op_code == ABNF.OPCODE_CONT and self.on_cont_message:
|
||||||
|
self._callback(self.on_data, frame.data, frame.opcode, frame.fin)
|
||||||
|
self._callback(self.on_cont_message, frame.data, frame.fin)
|
||||||
|
else:
|
||||||
|
data = frame.data
|
||||||
|
if op_code == ABNF.OPCODE_TEXT and not skip_utf8_validation:
|
||||||
|
data = data.decode("utf-8")
|
||||||
|
self._callback(self.on_data, data, frame.opcode, True)
|
||||||
|
self._callback(self.on_message, data)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def check() -> bool:
|
||||||
|
if self.ping_timeout:
|
||||||
|
has_timeout_expired = (
|
||||||
|
time.time() - self.last_ping_tm > self.ping_timeout
|
||||||
|
)
|
||||||
|
has_pong_not_arrived_after_last_ping = (
|
||||||
|
self.last_pong_tm - self.last_ping_tm < 0
|
||||||
|
)
|
||||||
|
has_pong_arrived_too_late = (
|
||||||
|
self.last_pong_tm - self.last_ping_tm > self.ping_timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
self.last_ping_tm
|
||||||
|
and has_timeout_expired
|
||||||
|
and (
|
||||||
|
has_pong_not_arrived_after_last_ping
|
||||||
|
or has_pong_arrived_too_late
|
||||||
|
)
|
||||||
|
):
|
||||||
|
raise WebSocketTimeoutException("ping/pong timed out")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def handleDisconnect(
|
||||||
|
e: Union[
|
||||||
|
WebSocketConnectionClosedException,
|
||||||
|
ConnectionRefusedError,
|
||||||
|
KeyboardInterrupt,
|
||||||
|
SystemExit,
|
||||||
|
Exception,
|
||||||
|
],
|
||||||
|
reconnecting: bool = False,
|
||||||
|
) -> bool:
|
||||||
|
self.has_errored = True
|
||||||
|
self._stop_ping_thread()
|
||||||
|
if not reconnecting:
|
||||||
|
self._callback(self.on_error, e)
|
||||||
|
|
||||||
|
if isinstance(e, (KeyboardInterrupt, SystemExit)):
|
||||||
|
teardown()
|
||||||
|
# Propagate further
|
||||||
|
raise
|
||||||
|
|
||||||
|
if reconnect:
|
||||||
|
_logging.info(f"{e} - reconnect")
|
||||||
|
if custom_dispatcher:
|
||||||
|
_logging.debug(
|
||||||
|
f"Calling custom dispatcher reconnect [{len(inspect.stack())} frames in stack]"
|
||||||
|
)
|
||||||
|
dispatcher.reconnect(reconnect, setSock)
|
||||||
|
else:
|
||||||
|
_logging.error(f"{e} - goodbye")
|
||||||
|
teardown()
|
||||||
|
|
||||||
|
custom_dispatcher = bool(dispatcher)
|
||||||
|
dispatcher = self.create_dispatcher(
|
||||||
|
ping_timeout, dispatcher, parse_url(self.url)[3]
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
setSock()
|
||||||
|
if not custom_dispatcher and reconnect:
|
||||||
|
while self.keep_running:
|
||||||
|
_logging.debug(
|
||||||
|
f"Calling dispatcher reconnect [{len(inspect.stack())} frames in stack]"
|
||||||
|
)
|
||||||
|
dispatcher.reconnect(reconnect, setSock)
|
||||||
|
except (KeyboardInterrupt, Exception) as e:
|
||||||
|
_logging.info(f"tearing down on exception {e}")
|
||||||
|
teardown()
|
||||||
|
finally:
|
||||||
|
if not custom_dispatcher:
|
||||||
|
# Ensure teardown was called before returning from run_forever
|
||||||
|
teardown()
|
||||||
|
|
||||||
|
return self.has_errored
|
||||||
|
|
||||||
|
def create_dispatcher(
|
||||||
|
self,
|
||||||
|
ping_timeout: Union[float, int, None],
|
||||||
|
dispatcher: Optional[DispatcherBase] = None,
|
||||||
|
is_ssl: bool = False,
|
||||||
|
) -> Union[Dispatcher, SSLDispatcher, WrappedDispatcher]:
|
||||||
|
if dispatcher: # If custom dispatcher is set, use WrappedDispatcher
|
||||||
|
return WrappedDispatcher(self, ping_timeout, dispatcher)
|
||||||
|
timeout = ping_timeout or 10
|
||||||
|
if is_ssl:
|
||||||
|
return SSLDispatcher(self, timeout)
|
||||||
|
return Dispatcher(self, timeout)
|
||||||
|
|
||||||
|
def _get_close_args(self, close_frame: ABNF) -> list:
|
||||||
|
"""
|
||||||
|
_get_close_args extracts the close code and reason from the close body
|
||||||
|
if it exists (RFC6455 says WebSocket Connection Close Code is optional)
|
||||||
|
"""
|
||||||
|
# Need to catch the case where close_frame is None
|
||||||
|
# Otherwise the following if statement causes an error
|
||||||
|
if not self.on_close or not close_frame:
|
||||||
|
return [None, None]
|
||||||
|
|
||||||
|
# Extract close frame status code
|
||||||
|
if close_frame.data and len(close_frame.data) >= 2:
|
||||||
|
close_status_code = 256 * int(close_frame.data[0]) + int(
|
||||||
|
close_frame.data[1]
|
||||||
|
)
|
||||||
|
reason = close_frame.data[2:]
|
||||||
|
if isinstance(reason, bytes):
|
||||||
|
reason = reason.decode("utf-8")
|
||||||
|
return [close_status_code, reason]
|
||||||
|
else:
|
||||||
|
# Most likely reached this because len(close_frame_data.data) < 2
|
||||||
|
return [None, None]
|
||||||
|
|
||||||
|
def _callback(self, callback, *args) -> None:
|
||||||
|
if callback:
|
||||||
|
try:
|
||||||
|
callback(self, *args)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
_logging.error(f"error from callback {callback}: {e}")
|
||||||
|
if self.on_error:
|
||||||
|
self.on_error(self, e)
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import http.cookies
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
"""
|
||||||
|
_cookiejar.py
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright 2024 engn33r
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleCookieJar:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.jar: dict = {}
|
||||||
|
|
||||||
|
def add(self, set_cookie: Optional[str]) -> None:
|
||||||
|
if set_cookie:
|
||||||
|
simple_cookie = http.cookies.SimpleCookie(set_cookie)
|
||||||
|
|
||||||
|
for v in simple_cookie.values():
|
||||||
|
if domain := v.get("domain"):
|
||||||
|
if not domain.startswith("."):
|
||||||
|
domain = f".{domain}"
|
||||||
|
cookie = (
|
||||||
|
self.jar.get(domain)
|
||||||
|
if self.jar.get(domain)
|
||||||
|
else http.cookies.SimpleCookie()
|
||||||
|
)
|
||||||
|
cookie.update(simple_cookie)
|
||||||
|
self.jar[domain.lower()] = cookie
|
||||||
|
|
||||||
|
def set(self, set_cookie: str) -> None:
|
||||||
|
if set_cookie:
|
||||||
|
simple_cookie = http.cookies.SimpleCookie(set_cookie)
|
||||||
|
|
||||||
|
for v in simple_cookie.values():
|
||||||
|
if domain := v.get("domain"):
|
||||||
|
if not domain.startswith("."):
|
||||||
|
domain = f".{domain}"
|
||||||
|
self.jar[domain.lower()] = simple_cookie
|
||||||
|
|
||||||
|
def get(self, host: str) -> str:
|
||||||
|
if not host:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
cookies = []
|
||||||
|
for domain, _ in self.jar.items():
|
||||||
|
host = host.lower()
|
||||||
|
if host.endswith(domain) or host == domain[1:]:
|
||||||
|
cookies.append(self.jar.get(domain))
|
||||||
|
|
||||||
|
return "; ".join(
|
||||||
|
filter(
|
||||||
|
None,
|
||||||
|
sorted(
|
||||||
|
[
|
||||||
|
f"{k}={v.value}"
|
||||||
|
for cookie in filter(None, cookies)
|
||||||
|
for k, v in cookie.items()
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,647 @@
|
|||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
# websocket modules
|
||||||
|
from ._abnf import ABNF, STATUS_NORMAL, continuous_frame, frame_buffer
|
||||||
|
from ._exceptions import WebSocketProtocolException, WebSocketConnectionClosedException
|
||||||
|
from ._handshake import SUPPORTED_REDIRECT_STATUSES, handshake
|
||||||
|
from ._http import connect, proxy_info
|
||||||
|
from ._logging import debug, error, trace, isEnabledForError, isEnabledForTrace
|
||||||
|
from ._socket import getdefaulttimeout, recv, send, sock_opt
|
||||||
|
from ._ssl_compat import ssl
|
||||||
|
from ._utils import NoLock
|
||||||
|
|
||||||
|
"""
|
||||||
|
_core.py
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright 2024 engn33r
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__all__ = ["WebSocket", "create_connection"]
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocket:
|
||||||
|
"""
|
||||||
|
Low level WebSocket interface.
|
||||||
|
|
||||||
|
This class is based on the WebSocket protocol `draft-hixie-thewebsocketprotocol-76 <http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76>`_
|
||||||
|
|
||||||
|
We can connect to the websocket server and send/receive data.
|
||||||
|
The following example is an echo client.
|
||||||
|
|
||||||
|
>>> import websocket
|
||||||
|
>>> ws = websocket.WebSocket()
|
||||||
|
>>> ws.connect("ws://echo.websocket.events")
|
||||||
|
>>> ws.recv()
|
||||||
|
'echo.websocket.events sponsored by Lob.com'
|
||||||
|
>>> ws.send("Hello, Server")
|
||||||
|
19
|
||||||
|
>>> ws.recv()
|
||||||
|
'Hello, Server'
|
||||||
|
>>> ws.close()
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
get_mask_key: func
|
||||||
|
A callable function to get new mask keys, see the
|
||||||
|
WebSocket.set_mask_key's docstring for more information.
|
||||||
|
sockopt: tuple
|
||||||
|
Values for socket.setsockopt.
|
||||||
|
sockopt must be tuple and each element is argument of sock.setsockopt.
|
||||||
|
sslopt: dict
|
||||||
|
Optional dict object for ssl socket options. See FAQ for details.
|
||||||
|
fire_cont_frame: bool
|
||||||
|
Fire recv event for each cont frame. Default is False.
|
||||||
|
enable_multithread: bool
|
||||||
|
If set to True, lock send method.
|
||||||
|
skip_utf8_validation: bool
|
||||||
|
Skip utf8 validation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
get_mask_key=None,
|
||||||
|
sockopt=None,
|
||||||
|
sslopt=None,
|
||||||
|
fire_cont_frame: bool = False,
|
||||||
|
enable_multithread: bool = True,
|
||||||
|
skip_utf8_validation: bool = False,
|
||||||
|
**_,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Initialize WebSocket object.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
sslopt: dict
|
||||||
|
Optional dict object for ssl socket options. See FAQ for details.
|
||||||
|
"""
|
||||||
|
self.sock_opt = sock_opt(sockopt, sslopt)
|
||||||
|
self.handshake_response = None
|
||||||
|
self.sock: Optional[socket.socket] = None
|
||||||
|
|
||||||
|
self.connected = False
|
||||||
|
self.get_mask_key = get_mask_key
|
||||||
|
# These buffer over the build-up of a single frame.
|
||||||
|
self.frame_buffer = frame_buffer(self._recv, skip_utf8_validation)
|
||||||
|
self.cont_frame = continuous_frame(fire_cont_frame, skip_utf8_validation)
|
||||||
|
|
||||||
|
if enable_multithread:
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
self.readlock = threading.Lock()
|
||||||
|
else:
|
||||||
|
self.lock = NoLock()
|
||||||
|
self.readlock = NoLock()
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
"""
|
||||||
|
Allow iteration over websocket, implying sequential `recv` executions.
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
yield self.recv()
|
||||||
|
|
||||||
|
def __next__(self):
|
||||||
|
return self.recv()
|
||||||
|
|
||||||
|
def next(self):
|
||||||
|
return self.__next__()
|
||||||
|
|
||||||
|
def fileno(self):
|
||||||
|
return self.sock.fileno()
|
||||||
|
|
||||||
|
def set_mask_key(self, func):
|
||||||
|
"""
|
||||||
|
Set function to create mask key. You can customize mask key generator.
|
||||||
|
Mainly, this is for testing purpose.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
func: func
|
||||||
|
callable object. the func takes 1 argument as integer.
|
||||||
|
The argument means length of mask key.
|
||||||
|
This func must return string(byte array),
|
||||||
|
which length is argument specified.
|
||||||
|
"""
|
||||||
|
self.get_mask_key = func
|
||||||
|
|
||||||
|
def gettimeout(self) -> Union[float, int, None]:
|
||||||
|
"""
|
||||||
|
Get the websocket timeout (in seconds) as an int or float
|
||||||
|
|
||||||
|
Returns
|
||||||
|
----------
|
||||||
|
timeout: int or float
|
||||||
|
returns timeout value (in seconds). This value could be either float/integer.
|
||||||
|
"""
|
||||||
|
return self.sock_opt.timeout
|
||||||
|
|
||||||
|
def settimeout(self, timeout: Union[float, int, None]):
|
||||||
|
"""
|
||||||
|
Set the timeout to the websocket.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
timeout: int or float
|
||||||
|
timeout time (in seconds). This value could be either float/integer.
|
||||||
|
"""
|
||||||
|
self.sock_opt.timeout = timeout
|
||||||
|
if self.sock:
|
||||||
|
self.sock.settimeout(timeout)
|
||||||
|
|
||||||
|
timeout = property(gettimeout, settimeout)
|
||||||
|
|
||||||
|
def getsubprotocol(self):
|
||||||
|
"""
|
||||||
|
Get subprotocol
|
||||||
|
"""
|
||||||
|
if self.handshake_response:
|
||||||
|
return self.handshake_response.subprotocol
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
subprotocol = property(getsubprotocol)
|
||||||
|
|
||||||
|
def getstatus(self):
|
||||||
|
"""
|
||||||
|
Get handshake status
|
||||||
|
"""
|
||||||
|
if self.handshake_response:
|
||||||
|
return self.handshake_response.status
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
status = property(getstatus)
|
||||||
|
|
||||||
|
def getheaders(self):
|
||||||
|
"""
|
||||||
|
Get handshake response header
|
||||||
|
"""
|
||||||
|
if self.handshake_response:
|
||||||
|
return self.handshake_response.headers
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def is_ssl(self):
|
||||||
|
try:
|
||||||
|
return isinstance(self.sock, ssl.SSLSocket)
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
headers = property(getheaders)
|
||||||
|
|
||||||
|
def connect(self, url, **options):
|
||||||
|
"""
|
||||||
|
Connect to url. url is websocket url scheme.
|
||||||
|
ie. ws://host:port/resource
|
||||||
|
You can customize using 'options'.
|
||||||
|
If you set "header" list object, you can set your own custom header.
|
||||||
|
|
||||||
|
>>> ws = WebSocket()
|
||||||
|
>>> ws.connect("ws://echo.websocket.events",
|
||||||
|
... header=["User-Agent: MyProgram",
|
||||||
|
... "x-custom: header"])
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
header: list or dict
|
||||||
|
Custom http header list or dict.
|
||||||
|
cookie: str
|
||||||
|
Cookie value.
|
||||||
|
origin: str
|
||||||
|
Custom origin url.
|
||||||
|
connection: str
|
||||||
|
Custom connection header value.
|
||||||
|
Default value "Upgrade" set in _handshake.py
|
||||||
|
suppress_origin: bool
|
||||||
|
Suppress outputting origin header.
|
||||||
|
host: str
|
||||||
|
Custom host header string.
|
||||||
|
timeout: int or float
|
||||||
|
Socket timeout time. This value is an integer or float.
|
||||||
|
If you set None for this value, it means "use default_timeout value"
|
||||||
|
http_proxy_host: str
|
||||||
|
HTTP proxy host name.
|
||||||
|
http_proxy_port: str or int
|
||||||
|
HTTP proxy port. Default is 80.
|
||||||
|
http_no_proxy: list
|
||||||
|
Whitelisted host names that don't use the proxy.
|
||||||
|
http_proxy_auth: tuple
|
||||||
|
HTTP proxy auth information. Tuple of username and password. Default is None.
|
||||||
|
http_proxy_timeout: int or float
|
||||||
|
HTTP proxy timeout, default is 60 sec as per python-socks.
|
||||||
|
redirect_limit: int
|
||||||
|
Number of redirects to follow.
|
||||||
|
subprotocols: list
|
||||||
|
List of available subprotocols. Default is None.
|
||||||
|
socket: socket
|
||||||
|
Pre-initialized stream socket.
|
||||||
|
"""
|
||||||
|
self.sock_opt.timeout = options.get("timeout", self.sock_opt.timeout)
|
||||||
|
self.sock, addrs = connect(
|
||||||
|
url, self.sock_opt, proxy_info(**options), options.pop("socket", None)
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.handshake_response = handshake(self.sock, url, *addrs, **options)
|
||||||
|
for _ in range(options.pop("redirect_limit", 3)):
|
||||||
|
if self.handshake_response.status in SUPPORTED_REDIRECT_STATUSES:
|
||||||
|
url = self.handshake_response.headers["location"]
|
||||||
|
self.sock.close()
|
||||||
|
self.sock, addrs = connect(
|
||||||
|
url,
|
||||||
|
self.sock_opt,
|
||||||
|
proxy_info(**options),
|
||||||
|
options.pop("socket", None),
|
||||||
|
)
|
||||||
|
self.handshake_response = handshake(
|
||||||
|
self.sock, url, *addrs, **options
|
||||||
|
)
|
||||||
|
self.connected = True
|
||||||
|
except:
|
||||||
|
if self.sock:
|
||||||
|
self.sock.close()
|
||||||
|
self.sock = None
|
||||||
|
raise
|
||||||
|
|
||||||
|
def send(self, payload: Union[bytes, str], opcode: int = ABNF.OPCODE_TEXT) -> int:
|
||||||
|
"""
|
||||||
|
Send the data as string.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
payload: str
|
||||||
|
Payload must be utf-8 string or unicode,
|
||||||
|
If the opcode is OPCODE_TEXT.
|
||||||
|
Otherwise, it must be string(byte array).
|
||||||
|
opcode: int
|
||||||
|
Operation code (opcode) to send.
|
||||||
|
"""
|
||||||
|
|
||||||
|
frame = ABNF.create_frame(payload, opcode)
|
||||||
|
return self.send_frame(frame)
|
||||||
|
|
||||||
|
def send_text(self, text_data: str) -> int:
|
||||||
|
"""
|
||||||
|
Sends UTF-8 encoded text.
|
||||||
|
"""
|
||||||
|
return self.send(text_data, ABNF.OPCODE_TEXT)
|
||||||
|
|
||||||
|
def send_bytes(self, data: Union[bytes, bytearray]) -> int:
|
||||||
|
"""
|
||||||
|
Sends a sequence of bytes.
|
||||||
|
"""
|
||||||
|
return self.send(data, ABNF.OPCODE_BINARY)
|
||||||
|
|
||||||
|
def send_frame(self, frame) -> int:
|
||||||
|
"""
|
||||||
|
Send the data frame.
|
||||||
|
|
||||||
|
>>> ws = create_connection("ws://echo.websocket.events")
|
||||||
|
>>> frame = ABNF.create_frame("Hello", ABNF.OPCODE_TEXT)
|
||||||
|
>>> ws.send_frame(frame)
|
||||||
|
>>> cont_frame = ABNF.create_frame("My name is ", ABNF.OPCODE_CONT, 0)
|
||||||
|
>>> ws.send_frame(frame)
|
||||||
|
>>> cont_frame = ABNF.create_frame("Foo Bar", ABNF.OPCODE_CONT, 1)
|
||||||
|
>>> ws.send_frame(frame)
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
frame: ABNF frame
|
||||||
|
frame data created by ABNF.create_frame
|
||||||
|
"""
|
||||||
|
if self.get_mask_key:
|
||||||
|
frame.get_mask_key = self.get_mask_key
|
||||||
|
data = frame.format()
|
||||||
|
length = len(data)
|
||||||
|
if isEnabledForTrace():
|
||||||
|
trace(f"++Sent raw: {repr(data)}")
|
||||||
|
trace(f"++Sent decoded: {frame.__str__()}")
|
||||||
|
with self.lock:
|
||||||
|
while data:
|
||||||
|
l = self._send(data)
|
||||||
|
data = data[l:]
|
||||||
|
|
||||||
|
return length
|
||||||
|
|
||||||
|
def send_binary(self, payload: bytes) -> int:
|
||||||
|
"""
|
||||||
|
Send a binary message (OPCODE_BINARY).
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
payload: bytes
|
||||||
|
payload of message to send.
|
||||||
|
"""
|
||||||
|
return self.send(payload, ABNF.OPCODE_BINARY)
|
||||||
|
|
||||||
|
def ping(self, payload: Union[str, bytes] = ""):
|
||||||
|
"""
|
||||||
|
Send ping data.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
payload: str
|
||||||
|
data payload to send server.
|
||||||
|
"""
|
||||||
|
if isinstance(payload, str):
|
||||||
|
payload = payload.encode("utf-8")
|
||||||
|
self.send(payload, ABNF.OPCODE_PING)
|
||||||
|
|
||||||
|
def pong(self, payload: Union[str, bytes] = ""):
|
||||||
|
"""
|
||||||
|
Send pong data.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
payload: str
|
||||||
|
data payload to send server.
|
||||||
|
"""
|
||||||
|
if isinstance(payload, str):
|
||||||
|
payload = payload.encode("utf-8")
|
||||||
|
self.send(payload, ABNF.OPCODE_PONG)
|
||||||
|
|
||||||
|
def recv(self) -> Union[str, bytes]:
|
||||||
|
"""
|
||||||
|
Receive string data(byte array) from the server.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
----------
|
||||||
|
data: string (byte array) value.
|
||||||
|
"""
|
||||||
|
with self.readlock:
|
||||||
|
opcode, data = self.recv_data()
|
||||||
|
if opcode == ABNF.OPCODE_TEXT:
|
||||||
|
data_received: Union[bytes, str] = data
|
||||||
|
if isinstance(data_received, bytes):
|
||||||
|
return data_received.decode("utf-8")
|
||||||
|
elif isinstance(data_received, str):
|
||||||
|
return data_received
|
||||||
|
elif opcode == ABNF.OPCODE_BINARY:
|
||||||
|
data_binary: bytes = data
|
||||||
|
return data_binary
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def recv_data(self, control_frame: bool = False) -> tuple:
|
||||||
|
"""
|
||||||
|
Receive data with operation code.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
control_frame: bool
|
||||||
|
a boolean flag indicating whether to return control frame
|
||||||
|
data, defaults to False
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
opcode, frame.data: tuple
|
||||||
|
tuple of operation code and string(byte array) value.
|
||||||
|
"""
|
||||||
|
opcode, frame = self.recv_data_frame(control_frame)
|
||||||
|
return opcode, frame.data
|
||||||
|
|
||||||
|
def recv_data_frame(self, control_frame: bool = False) -> tuple:
|
||||||
|
"""
|
||||||
|
Receive data with operation code.
|
||||||
|
|
||||||
|
If a valid ping message is received, a pong response is sent.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
control_frame: bool
|
||||||
|
a boolean flag indicating whether to return control frame
|
||||||
|
data, defaults to False
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
frame.opcode, frame: tuple
|
||||||
|
tuple of operation code and string(byte array) value.
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
frame = self.recv_frame()
|
||||||
|
if isEnabledForTrace():
|
||||||
|
trace(f"++Rcv raw: {repr(frame.format())}")
|
||||||
|
trace(f"++Rcv decoded: {frame.__str__()}")
|
||||||
|
if not frame:
|
||||||
|
# handle error:
|
||||||
|
# 'NoneType' object has no attribute 'opcode'
|
||||||
|
raise WebSocketProtocolException(f"Not a valid frame {frame}")
|
||||||
|
elif frame.opcode in (
|
||||||
|
ABNF.OPCODE_TEXT,
|
||||||
|
ABNF.OPCODE_BINARY,
|
||||||
|
ABNF.OPCODE_CONT,
|
||||||
|
):
|
||||||
|
self.cont_frame.validate(frame)
|
||||||
|
self.cont_frame.add(frame)
|
||||||
|
|
||||||
|
if self.cont_frame.is_fire(frame):
|
||||||
|
return self.cont_frame.extract(frame)
|
||||||
|
|
||||||
|
elif frame.opcode == ABNF.OPCODE_CLOSE:
|
||||||
|
self.send_close()
|
||||||
|
return frame.opcode, frame
|
||||||
|
elif frame.opcode == ABNF.OPCODE_PING:
|
||||||
|
if len(frame.data) < 126:
|
||||||
|
self.pong(frame.data)
|
||||||
|
else:
|
||||||
|
raise WebSocketProtocolException("Ping message is too long")
|
||||||
|
if control_frame:
|
||||||
|
return frame.opcode, frame
|
||||||
|
elif frame.opcode == ABNF.OPCODE_PONG:
|
||||||
|
if control_frame:
|
||||||
|
return frame.opcode, frame
|
||||||
|
|
||||||
|
def recv_frame(self):
|
||||||
|
"""
|
||||||
|
Receive data as frame from server.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
self.frame_buffer.recv_frame(): ABNF frame object
|
||||||
|
"""
|
||||||
|
return self.frame_buffer.recv_frame()
|
||||||
|
|
||||||
|
def send_close(self, status: int = STATUS_NORMAL, reason: bytes = b""):
|
||||||
|
"""
|
||||||
|
Send close data to the server.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
status: int
|
||||||
|
Status code to send. See STATUS_XXX.
|
||||||
|
reason: str or bytes
|
||||||
|
The reason to close. This must be string or UTF-8 bytes.
|
||||||
|
"""
|
||||||
|
if status < 0 or status >= ABNF.LENGTH_16:
|
||||||
|
raise ValueError("code is invalid range")
|
||||||
|
self.connected = False
|
||||||
|
self.send(struct.pack("!H", status) + reason, ABNF.OPCODE_CLOSE)
|
||||||
|
|
||||||
|
def close(self, status: int = STATUS_NORMAL, reason: bytes = b"", timeout: int = 3):
|
||||||
|
"""
|
||||||
|
Close Websocket object
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
status: int
|
||||||
|
Status code to send. See VALID_CLOSE_STATUS in ABNF.
|
||||||
|
reason: bytes
|
||||||
|
The reason to close in UTF-8.
|
||||||
|
timeout: int or float
|
||||||
|
Timeout until receive a close frame.
|
||||||
|
If None, it will wait forever until receive a close frame.
|
||||||
|
"""
|
||||||
|
if not self.connected:
|
||||||
|
return
|
||||||
|
if status < 0 or status >= ABNF.LENGTH_16:
|
||||||
|
raise ValueError("code is invalid range")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.connected = False
|
||||||
|
self.send(struct.pack("!H", status) + reason, ABNF.OPCODE_CLOSE)
|
||||||
|
sock_timeout = self.sock.gettimeout()
|
||||||
|
self.sock.settimeout(timeout)
|
||||||
|
start_time = time.time()
|
||||||
|
while timeout is None or time.time() - start_time < timeout:
|
||||||
|
try:
|
||||||
|
frame = self.recv_frame()
|
||||||
|
if frame.opcode != ABNF.OPCODE_CLOSE:
|
||||||
|
continue
|
||||||
|
if isEnabledForError():
|
||||||
|
recv_status = struct.unpack("!H", frame.data[0:2])[0]
|
||||||
|
if recv_status >= 3000 and recv_status <= 4999:
|
||||||
|
debug(f"close status: {repr(recv_status)}")
|
||||||
|
elif recv_status != STATUS_NORMAL:
|
||||||
|
error(f"close status: {repr(recv_status)}")
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
break
|
||||||
|
self.sock.settimeout(sock_timeout)
|
||||||
|
self.sock.shutdown(socket.SHUT_RDWR)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.shutdown()
|
||||||
|
|
||||||
|
def abort(self):
|
||||||
|
"""
|
||||||
|
Low-level asynchronous abort, wakes up other threads that are waiting in recv_*
|
||||||
|
"""
|
||||||
|
if self.connected:
|
||||||
|
self.sock.shutdown(socket.SHUT_RDWR)
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
"""
|
||||||
|
close socket, immediately.
|
||||||
|
"""
|
||||||
|
if self.sock:
|
||||||
|
self.sock.close()
|
||||||
|
self.sock = None
|
||||||
|
self.connected = False
|
||||||
|
|
||||||
|
def _send(self, data: Union[str, bytes]):
|
||||||
|
return send(self.sock, data)
|
||||||
|
|
||||||
|
def _recv(self, bufsize):
|
||||||
|
try:
|
||||||
|
return recv(self.sock, bufsize)
|
||||||
|
except WebSocketConnectionClosedException:
|
||||||
|
if self.sock:
|
||||||
|
self.sock.close()
|
||||||
|
self.sock = None
|
||||||
|
self.connected = False
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def create_connection(url: str, timeout=None, class_=WebSocket, **options):
|
||||||
|
"""
|
||||||
|
Connect to url and return websocket object.
|
||||||
|
|
||||||
|
Connect to url and return the WebSocket object.
|
||||||
|
Passing optional timeout parameter will set the timeout on the socket.
|
||||||
|
If no timeout is supplied,
|
||||||
|
the global default timeout setting returned by getdefaulttimeout() is used.
|
||||||
|
You can customize using 'options'.
|
||||||
|
If you set "header" list object, you can set your own custom header.
|
||||||
|
|
||||||
|
>>> conn = create_connection("ws://echo.websocket.events",
|
||||||
|
... header=["User-Agent: MyProgram",
|
||||||
|
... "x-custom: header"])
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
class_: class
|
||||||
|
class to instantiate when creating the connection. It has to implement
|
||||||
|
settimeout and connect. It's __init__ should be compatible with
|
||||||
|
WebSocket.__init__, i.e. accept all of it's kwargs.
|
||||||
|
header: list or dict
|
||||||
|
custom http header list or dict.
|
||||||
|
cookie: str
|
||||||
|
Cookie value.
|
||||||
|
origin: str
|
||||||
|
custom origin url.
|
||||||
|
suppress_origin: bool
|
||||||
|
suppress outputting origin header.
|
||||||
|
host: str
|
||||||
|
custom host header string.
|
||||||
|
timeout: int or float
|
||||||
|
socket timeout time. This value could be either float/integer.
|
||||||
|
If set to None, it uses the default_timeout value.
|
||||||
|
http_proxy_host: str
|
||||||
|
HTTP proxy host name.
|
||||||
|
http_proxy_port: str or int
|
||||||
|
HTTP proxy port. If not set, set to 80.
|
||||||
|
http_no_proxy: list
|
||||||
|
Whitelisted host names that don't use the proxy.
|
||||||
|
http_proxy_auth: tuple
|
||||||
|
HTTP proxy auth information. tuple of username and password. Default is None.
|
||||||
|
http_proxy_timeout: int or float
|
||||||
|
HTTP proxy timeout, default is 60 sec as per python-socks.
|
||||||
|
enable_multithread: bool
|
||||||
|
Enable lock for multithread.
|
||||||
|
redirect_limit: int
|
||||||
|
Number of redirects to follow.
|
||||||
|
sockopt: tuple
|
||||||
|
Values for socket.setsockopt.
|
||||||
|
sockopt must be a tuple and each element is an argument of sock.setsockopt.
|
||||||
|
sslopt: dict
|
||||||
|
Optional dict object for ssl socket options. See FAQ for details.
|
||||||
|
subprotocols: list
|
||||||
|
List of available subprotocols. Default is None.
|
||||||
|
skip_utf8_validation: bool
|
||||||
|
Skip utf8 validation.
|
||||||
|
socket: socket
|
||||||
|
Pre-initialized stream socket.
|
||||||
|
"""
|
||||||
|
sockopt = options.pop("sockopt", [])
|
||||||
|
sslopt = options.pop("sslopt", {})
|
||||||
|
fire_cont_frame = options.pop("fire_cont_frame", False)
|
||||||
|
enable_multithread = options.pop("enable_multithread", True)
|
||||||
|
skip_utf8_validation = options.pop("skip_utf8_validation", False)
|
||||||
|
websock = class_(
|
||||||
|
sockopt=sockopt,
|
||||||
|
sslopt=sslopt,
|
||||||
|
fire_cont_frame=fire_cont_frame,
|
||||||
|
enable_multithread=enable_multithread,
|
||||||
|
skip_utf8_validation=skip_utf8_validation,
|
||||||
|
**options,
|
||||||
|
)
|
||||||
|
websock.settimeout(timeout if timeout is not None else getdefaulttimeout())
|
||||||
|
websock.connect(url, **options)
|
||||||
|
return websock
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
"""
|
||||||
|
_exceptions.py
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright 2024 engn33r
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketException(Exception):
|
||||||
|
"""
|
||||||
|
WebSocket exception class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketProtocolException(WebSocketException):
|
||||||
|
"""
|
||||||
|
If the WebSocket protocol is invalid, this exception will be raised.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketPayloadException(WebSocketException):
|
||||||
|
"""
|
||||||
|
If the WebSocket payload is invalid, this exception will be raised.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketConnectionClosedException(WebSocketException):
|
||||||
|
"""
|
||||||
|
If remote host closed the connection or some network error happened,
|
||||||
|
this exception will be raised.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketTimeoutException(WebSocketException):
|
||||||
|
"""
|
||||||
|
WebSocketTimeoutException will be raised at socket timeout during read/write data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketProxyException(WebSocketException):
|
||||||
|
"""
|
||||||
|
WebSocketProxyException will be raised when proxy error occurred.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketBadStatusException(WebSocketException):
|
||||||
|
"""
|
||||||
|
WebSocketBadStatusException will be raised when we get bad handshake status code.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str,
|
||||||
|
status_code: int,
|
||||||
|
status_message=None,
|
||||||
|
resp_headers=None,
|
||||||
|
resp_body=None,
|
||||||
|
):
|
||||||
|
super().__init__(message)
|
||||||
|
self.status_code = status_code
|
||||||
|
self.resp_headers = resp_headers
|
||||||
|
self.resp_body = resp_body
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketAddressException(WebSocketException):
|
||||||
|
"""
|
||||||
|
If the websocket address info cannot be found, this exception will be raised.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
"""
|
||||||
|
_handshake.py
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright 2024 engn33r
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import os
|
||||||
|
from base64 import encodebytes as base64encode
|
||||||
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
from ._cookiejar import SimpleCookieJar
|
||||||
|
from ._exceptions import WebSocketException, WebSocketBadStatusException
|
||||||
|
from ._http import read_headers
|
||||||
|
from ._logging import dump, error
|
||||||
|
from ._socket import send
|
||||||
|
|
||||||
|
__all__ = ["handshake_response", "handshake", "SUPPORTED_REDIRECT_STATUSES"]
|
||||||
|
|
||||||
|
# websocket supported version.
|
||||||
|
VERSION = 13
|
||||||
|
|
||||||
|
SUPPORTED_REDIRECT_STATUSES = (
|
||||||
|
HTTPStatus.MOVED_PERMANENTLY,
|
||||||
|
HTTPStatus.FOUND,
|
||||||
|
HTTPStatus.SEE_OTHER,
|
||||||
|
HTTPStatus.TEMPORARY_REDIRECT,
|
||||||
|
HTTPStatus.PERMANENT_REDIRECT,
|
||||||
|
)
|
||||||
|
SUCCESS_STATUSES = SUPPORTED_REDIRECT_STATUSES + (HTTPStatus.SWITCHING_PROTOCOLS,)
|
||||||
|
|
||||||
|
CookieJar = SimpleCookieJar()
|
||||||
|
|
||||||
|
|
||||||
|
class handshake_response:
|
||||||
|
def __init__(self, status: int, headers: dict, subprotocol):
|
||||||
|
self.status = status
|
||||||
|
self.headers = headers
|
||||||
|
self.subprotocol = subprotocol
|
||||||
|
CookieJar.add(headers.get("set-cookie"))
|
||||||
|
|
||||||
|
|
||||||
|
def handshake(
|
||||||
|
sock, url: str, hostname: str, port: int, resource: str, **options
|
||||||
|
) -> handshake_response:
|
||||||
|
headers, key = _get_handshake_headers(resource, url, hostname, port, options)
|
||||||
|
|
||||||
|
header_str = "\r\n".join(headers)
|
||||||
|
send(sock, header_str)
|
||||||
|
dump("request header", header_str)
|
||||||
|
|
||||||
|
status, resp = _get_resp_headers(sock)
|
||||||
|
if status in SUPPORTED_REDIRECT_STATUSES:
|
||||||
|
return handshake_response(status, resp, None)
|
||||||
|
success, subproto = _validate(resp, key, options.get("subprotocols"))
|
||||||
|
if not success:
|
||||||
|
raise WebSocketException("Invalid WebSocket Header")
|
||||||
|
|
||||||
|
return handshake_response(status, resp, subproto)
|
||||||
|
|
||||||
|
|
||||||
|
def _pack_hostname(hostname: str) -> str:
|
||||||
|
# IPv6 address
|
||||||
|
if ":" in hostname:
|
||||||
|
return f"[{hostname}]"
|
||||||
|
return hostname
|
||||||
|
|
||||||
|
|
||||||
|
def _get_handshake_headers(
|
||||||
|
resource: str, url: str, host: str, port: int, options: dict
|
||||||
|
) -> tuple:
|
||||||
|
headers = [f"GET {resource} HTTP/1.1", "Upgrade: websocket"]
|
||||||
|
if port in [80, 443]:
|
||||||
|
hostport = _pack_hostname(host)
|
||||||
|
else:
|
||||||
|
hostport = f"{_pack_hostname(host)}:{port}"
|
||||||
|
if options.get("host"):
|
||||||
|
headers.append(f'Host: {options["host"]}')
|
||||||
|
else:
|
||||||
|
headers.append(f"Host: {hostport}")
|
||||||
|
|
||||||
|
# scheme indicates whether http or https is used in Origin
|
||||||
|
# The same approach is used in parse_url of _url.py to set default port
|
||||||
|
scheme, url = url.split(":", 1)
|
||||||
|
if not options.get("suppress_origin"):
|
||||||
|
if "origin" in options and options["origin"] is not None:
|
||||||
|
headers.append(f'Origin: {options["origin"]}')
|
||||||
|
elif scheme == "wss":
|
||||||
|
headers.append(f"Origin: https://{hostport}")
|
||||||
|
else:
|
||||||
|
headers.append(f"Origin: http://{hostport}")
|
||||||
|
|
||||||
|
key = _create_sec_websocket_key()
|
||||||
|
|
||||||
|
# Append Sec-WebSocket-Key & Sec-WebSocket-Version if not manually specified
|
||||||
|
if not options.get("header") or "Sec-WebSocket-Key" not in options["header"]:
|
||||||
|
headers.append(f"Sec-WebSocket-Key: {key}")
|
||||||
|
else:
|
||||||
|
key = options["header"]["Sec-WebSocket-Key"]
|
||||||
|
|
||||||
|
if not options.get("header") or "Sec-WebSocket-Version" not in options["header"]:
|
||||||
|
headers.append(f"Sec-WebSocket-Version: {VERSION}")
|
||||||
|
|
||||||
|
if not options.get("connection"):
|
||||||
|
headers.append("Connection: Upgrade")
|
||||||
|
else:
|
||||||
|
headers.append(options["connection"])
|
||||||
|
|
||||||
|
if subprotocols := options.get("subprotocols"):
|
||||||
|
headers.append(f'Sec-WebSocket-Protocol: {",".join(subprotocols)}')
|
||||||
|
|
||||||
|
if header := options.get("header"):
|
||||||
|
if isinstance(header, dict):
|
||||||
|
header = [": ".join([k, v]) for k, v in header.items() if v is not None]
|
||||||
|
headers.extend(header)
|
||||||
|
|
||||||
|
server_cookie = CookieJar.get(host)
|
||||||
|
client_cookie = options.get("cookie", None)
|
||||||
|
|
||||||
|
if cookie := "; ".join(filter(None, [server_cookie, client_cookie])):
|
||||||
|
headers.append(f"Cookie: {cookie}")
|
||||||
|
|
||||||
|
headers.extend(("", ""))
|
||||||
|
return headers, key
|
||||||
|
|
||||||
|
|
||||||
|
def _get_resp_headers(sock, success_statuses: tuple = SUCCESS_STATUSES) -> tuple:
|
||||||
|
status, resp_headers, status_message = read_headers(sock)
|
||||||
|
if status not in success_statuses:
|
||||||
|
content_len = resp_headers.get("content-length")
|
||||||
|
if content_len:
|
||||||
|
response_body = sock.recv(
|
||||||
|
int(content_len)
|
||||||
|
) # read the body of the HTTP error message response and include it in the exception
|
||||||
|
else:
|
||||||
|
response_body = None
|
||||||
|
raise WebSocketBadStatusException(
|
||||||
|
f"Handshake status {status} {status_message} -+-+- {resp_headers} -+-+- {response_body}",
|
||||||
|
status,
|
||||||
|
status_message,
|
||||||
|
resp_headers,
|
||||||
|
response_body,
|
||||||
|
)
|
||||||
|
return status, resp_headers
|
||||||
|
|
||||||
|
|
||||||
|
_HEADERS_TO_CHECK = {
|
||||||
|
"upgrade": "websocket",
|
||||||
|
"connection": "upgrade",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _validate(headers, key: str, subprotocols) -> tuple:
|
||||||
|
subproto = None
|
||||||
|
for k, v in _HEADERS_TO_CHECK.items():
|
||||||
|
r = headers.get(k, None)
|
||||||
|
if not r:
|
||||||
|
return False, None
|
||||||
|
r = [x.strip().lower() for x in r.split(",")]
|
||||||
|
if v not in r:
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
if subprotocols:
|
||||||
|
subproto = headers.get("sec-websocket-protocol", None)
|
||||||
|
if not subproto or subproto.lower() not in [s.lower() for s in subprotocols]:
|
||||||
|
error(f"Invalid subprotocol: {subprotocols}")
|
||||||
|
return False, None
|
||||||
|
subproto = subproto.lower()
|
||||||
|
|
||||||
|
result = headers.get("sec-websocket-accept", None)
|
||||||
|
if not result:
|
||||||
|
return False, None
|
||||||
|
result = result.lower()
|
||||||
|
|
||||||
|
if isinstance(result, str):
|
||||||
|
result = result.encode("utf-8")
|
||||||
|
|
||||||
|
value = f"{key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11".encode("utf-8")
|
||||||
|
hashed = base64encode(hashlib.sha1(value).digest()).strip().lower()
|
||||||
|
|
||||||
|
if hmac.compare_digest(hashed, result):
|
||||||
|
return True, subproto
|
||||||
|
else:
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
|
||||||
|
def _create_sec_websocket_key() -> str:
|
||||||
|
randomness = os.urandom(16)
|
||||||
|
return base64encode(randomness).decode("utf-8").strip()
|
||||||
@@ -0,0 +1,374 @@
|
|||||||
|
"""
|
||||||
|
_http.py
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright 2024 engn33r
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import errno
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
from base64 import encodebytes as base64encode
|
||||||
|
|
||||||
|
from ._exceptions import (
|
||||||
|
WebSocketAddressException,
|
||||||
|
WebSocketException,
|
||||||
|
WebSocketProxyException,
|
||||||
|
)
|
||||||
|
from ._logging import debug, dump, trace
|
||||||
|
from ._socket import DEFAULT_SOCKET_OPTION, recv_line, send
|
||||||
|
from ._ssl_compat import HAVE_SSL, ssl
|
||||||
|
from ._url import get_proxy_info, parse_url
|
||||||
|
|
||||||
|
__all__ = ["proxy_info", "connect", "read_headers"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
from python_socks._errors import *
|
||||||
|
from python_socks._types import ProxyType
|
||||||
|
from python_socks.sync import Proxy
|
||||||
|
|
||||||
|
HAVE_PYTHON_SOCKS = True
|
||||||
|
except:
|
||||||
|
HAVE_PYTHON_SOCKS = False
|
||||||
|
|
||||||
|
class ProxyError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ProxyTimeoutError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ProxyConnectionError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class proxy_info:
|
||||||
|
def __init__(self, **options):
|
||||||
|
self.proxy_host = options.get("http_proxy_host", None)
|
||||||
|
if self.proxy_host:
|
||||||
|
self.proxy_port = options.get("http_proxy_port", 0)
|
||||||
|
self.auth = options.get("http_proxy_auth", None)
|
||||||
|
self.no_proxy = options.get("http_no_proxy", None)
|
||||||
|
self.proxy_protocol = options.get("proxy_type", "http")
|
||||||
|
# Note: If timeout not specified, default python-socks timeout is 60 seconds
|
||||||
|
self.proxy_timeout = options.get("http_proxy_timeout", None)
|
||||||
|
if self.proxy_protocol not in [
|
||||||
|
"http",
|
||||||
|
"socks4",
|
||||||
|
"socks4a",
|
||||||
|
"socks5",
|
||||||
|
"socks5h",
|
||||||
|
]:
|
||||||
|
raise ProxyError(
|
||||||
|
"Only http, socks4, socks5 proxy protocols are supported"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.proxy_port = 0
|
||||||
|
self.auth = None
|
||||||
|
self.no_proxy = None
|
||||||
|
self.proxy_protocol = "http"
|
||||||
|
|
||||||
|
|
||||||
|
def _start_proxied_socket(url: str, options, proxy) -> tuple:
|
||||||
|
if not HAVE_PYTHON_SOCKS:
|
||||||
|
raise WebSocketException(
|
||||||
|
"Python Socks is needed for SOCKS proxying but is not available"
|
||||||
|
)
|
||||||
|
|
||||||
|
hostname, port, resource, is_secure = parse_url(url)
|
||||||
|
|
||||||
|
if proxy.proxy_protocol == "socks4":
|
||||||
|
rdns = False
|
||||||
|
proxy_type = ProxyType.SOCKS4
|
||||||
|
# socks4a sends DNS through proxy
|
||||||
|
elif proxy.proxy_protocol == "socks4a":
|
||||||
|
rdns = True
|
||||||
|
proxy_type = ProxyType.SOCKS4
|
||||||
|
elif proxy.proxy_protocol == "socks5":
|
||||||
|
rdns = False
|
||||||
|
proxy_type = ProxyType.SOCKS5
|
||||||
|
# socks5h sends DNS through proxy
|
||||||
|
elif proxy.proxy_protocol == "socks5h":
|
||||||
|
rdns = True
|
||||||
|
proxy_type = ProxyType.SOCKS5
|
||||||
|
|
||||||
|
ws_proxy = Proxy.create(
|
||||||
|
proxy_type=proxy_type,
|
||||||
|
host=proxy.proxy_host,
|
||||||
|
port=int(proxy.proxy_port),
|
||||||
|
username=proxy.auth[0] if proxy.auth else None,
|
||||||
|
password=proxy.auth[1] if proxy.auth else None,
|
||||||
|
rdns=rdns,
|
||||||
|
)
|
||||||
|
|
||||||
|
sock = ws_proxy.connect(hostname, port, timeout=proxy.proxy_timeout)
|
||||||
|
|
||||||
|
if is_secure:
|
||||||
|
if HAVE_SSL:
|
||||||
|
sock = _ssl_socket(sock, options.sslopt, hostname)
|
||||||
|
else:
|
||||||
|
raise WebSocketException("SSL not available.")
|
||||||
|
|
||||||
|
return sock, (hostname, port, resource)
|
||||||
|
|
||||||
|
|
||||||
|
def connect(url: str, options, proxy, socket):
|
||||||
|
# Use _start_proxied_socket() only for socks4 or socks5 proxy
|
||||||
|
# Use _tunnel() for http proxy
|
||||||
|
# TODO: Use python-socks for http protocol also, to standardize flow
|
||||||
|
if proxy.proxy_host and not socket and proxy.proxy_protocol != "http":
|
||||||
|
return _start_proxied_socket(url, options, proxy)
|
||||||
|
|
||||||
|
hostname, port_from_url, resource, is_secure = parse_url(url)
|
||||||
|
|
||||||
|
if socket:
|
||||||
|
return socket, (hostname, port_from_url, resource)
|
||||||
|
|
||||||
|
addrinfo_list, need_tunnel, auth = _get_addrinfo_list(
|
||||||
|
hostname, port_from_url, is_secure, proxy
|
||||||
|
)
|
||||||
|
if not addrinfo_list:
|
||||||
|
raise WebSocketException(f"Host not found.: {hostname}:{port_from_url}")
|
||||||
|
|
||||||
|
sock = None
|
||||||
|
try:
|
||||||
|
sock = _open_socket(addrinfo_list, options.sockopt, options.timeout)
|
||||||
|
if need_tunnel:
|
||||||
|
sock = _tunnel(sock, hostname, port_from_url, auth)
|
||||||
|
|
||||||
|
if is_secure:
|
||||||
|
if HAVE_SSL:
|
||||||
|
sock = _ssl_socket(sock, options.sslopt, hostname)
|
||||||
|
else:
|
||||||
|
raise WebSocketException("SSL not available.")
|
||||||
|
|
||||||
|
return sock, (hostname, port_from_url, resource)
|
||||||
|
except:
|
||||||
|
if sock:
|
||||||
|
sock.close()
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def _get_addrinfo_list(hostname, port: int, is_secure: bool, proxy) -> tuple:
|
||||||
|
phost, pport, pauth = get_proxy_info(
|
||||||
|
hostname,
|
||||||
|
is_secure,
|
||||||
|
proxy.proxy_host,
|
||||||
|
proxy.proxy_port,
|
||||||
|
proxy.auth,
|
||||||
|
proxy.no_proxy,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
# when running on windows 10, getaddrinfo without socktype returns a socktype 0.
|
||||||
|
# This generates an error exception: `_on_error: exception Socket type must be stream or datagram, not 0`
|
||||||
|
# or `OSError: [Errno 22] Invalid argument` when creating socket. Force the socket type to SOCK_STREAM.
|
||||||
|
if not phost:
|
||||||
|
addrinfo_list = socket.getaddrinfo(
|
||||||
|
hostname, port, 0, socket.SOCK_STREAM, socket.SOL_TCP
|
||||||
|
)
|
||||||
|
return addrinfo_list, False, None
|
||||||
|
else:
|
||||||
|
pport = pport and pport or 80
|
||||||
|
# when running on windows 10, the getaddrinfo used above
|
||||||
|
# returns a socktype 0. This generates an error exception:
|
||||||
|
# _on_error: exception Socket type must be stream or datagram, not 0
|
||||||
|
# Force the socket type to SOCK_STREAM
|
||||||
|
addrinfo_list = socket.getaddrinfo(
|
||||||
|
phost, pport, 0, socket.SOCK_STREAM, socket.SOL_TCP
|
||||||
|
)
|
||||||
|
return addrinfo_list, True, pauth
|
||||||
|
except socket.gaierror as e:
|
||||||
|
raise WebSocketAddressException(e)
|
||||||
|
|
||||||
|
|
||||||
|
def _open_socket(addrinfo_list, sockopt, timeout):
|
||||||
|
err = None
|
||||||
|
for addrinfo in addrinfo_list:
|
||||||
|
family, socktype, proto = addrinfo[:3]
|
||||||
|
sock = socket.socket(family, socktype, proto)
|
||||||
|
sock.settimeout(timeout)
|
||||||
|
for opts in DEFAULT_SOCKET_OPTION:
|
||||||
|
sock.setsockopt(*opts)
|
||||||
|
for opts in sockopt:
|
||||||
|
sock.setsockopt(*opts)
|
||||||
|
|
||||||
|
address = addrinfo[4]
|
||||||
|
err = None
|
||||||
|
while not err:
|
||||||
|
try:
|
||||||
|
sock.connect(address)
|
||||||
|
except socket.error as error:
|
||||||
|
sock.close()
|
||||||
|
error.remote_ip = str(address[0])
|
||||||
|
try:
|
||||||
|
eConnRefused = (
|
||||||
|
errno.ECONNREFUSED,
|
||||||
|
errno.WSAECONNREFUSED,
|
||||||
|
errno.ENETUNREACH,
|
||||||
|
)
|
||||||
|
except AttributeError:
|
||||||
|
eConnRefused = (errno.ECONNREFUSED, errno.ENETUNREACH)
|
||||||
|
if error.errno not in eConnRefused:
|
||||||
|
raise error
|
||||||
|
err = error
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if err:
|
||||||
|
raise err
|
||||||
|
|
||||||
|
return sock
|
||||||
|
|
||||||
|
|
||||||
|
def _wrap_sni_socket(sock: socket.socket, sslopt: dict, hostname, check_hostname):
|
||||||
|
context = sslopt.get("context", None)
|
||||||
|
if not context:
|
||||||
|
context = ssl.SSLContext(sslopt.get("ssl_version", ssl.PROTOCOL_TLS_CLIENT))
|
||||||
|
# Non default context need to manually enable SSLKEYLOGFILE support by setting the keylog_filename attribute.
|
||||||
|
# For more details see also:
|
||||||
|
# * https://docs.python.org/3.8/library/ssl.html?highlight=sslkeylogfile#context-creation
|
||||||
|
# * https://docs.python.org/3.8/library/ssl.html?highlight=sslkeylogfile#ssl.SSLContext.keylog_filename
|
||||||
|
context.keylog_filename = os.environ.get("SSLKEYLOGFILE", None)
|
||||||
|
|
||||||
|
if sslopt.get("cert_reqs", ssl.CERT_NONE) != ssl.CERT_NONE:
|
||||||
|
cafile = sslopt.get("ca_certs", None)
|
||||||
|
capath = sslopt.get("ca_cert_path", None)
|
||||||
|
if cafile or capath:
|
||||||
|
context.load_verify_locations(cafile=cafile, capath=capath)
|
||||||
|
elif hasattr(context, "load_default_certs"):
|
||||||
|
context.load_default_certs(ssl.Purpose.SERVER_AUTH)
|
||||||
|
if sslopt.get("certfile", None):
|
||||||
|
context.load_cert_chain(
|
||||||
|
sslopt["certfile"],
|
||||||
|
sslopt.get("keyfile", None),
|
||||||
|
sslopt.get("password", None),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Python 3.10 switch to PROTOCOL_TLS_CLIENT defaults to "cert_reqs = ssl.CERT_REQUIRED" and "check_hostname = True"
|
||||||
|
# If both disabled, set check_hostname before verify_mode
|
||||||
|
# see https://github.com/liris/websocket-client/commit/b96a2e8fa765753e82eea531adb19716b52ca3ca#commitcomment-10803153
|
||||||
|
if sslopt.get("cert_reqs", ssl.CERT_NONE) == ssl.CERT_NONE and not sslopt.get(
|
||||||
|
"check_hostname", False
|
||||||
|
):
|
||||||
|
context.check_hostname = False
|
||||||
|
context.verify_mode = ssl.CERT_NONE
|
||||||
|
else:
|
||||||
|
context.check_hostname = sslopt.get("check_hostname", True)
|
||||||
|
context.verify_mode = sslopt.get("cert_reqs", ssl.CERT_REQUIRED)
|
||||||
|
|
||||||
|
if "ciphers" in sslopt:
|
||||||
|
context.set_ciphers(sslopt["ciphers"])
|
||||||
|
if "cert_chain" in sslopt:
|
||||||
|
certfile, keyfile, password = sslopt["cert_chain"]
|
||||||
|
context.load_cert_chain(certfile, keyfile, password)
|
||||||
|
if "ecdh_curve" in sslopt:
|
||||||
|
context.set_ecdh_curve(sslopt["ecdh_curve"])
|
||||||
|
|
||||||
|
return context.wrap_socket(
|
||||||
|
sock,
|
||||||
|
do_handshake_on_connect=sslopt.get("do_handshake_on_connect", True),
|
||||||
|
suppress_ragged_eofs=sslopt.get("suppress_ragged_eofs", True),
|
||||||
|
server_hostname=hostname,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _ssl_socket(sock: socket.socket, user_sslopt: dict, hostname):
|
||||||
|
sslopt: dict = {"cert_reqs": ssl.CERT_REQUIRED}
|
||||||
|
sslopt.update(user_sslopt)
|
||||||
|
|
||||||
|
cert_path = os.environ.get("WEBSOCKET_CLIENT_CA_BUNDLE")
|
||||||
|
if (
|
||||||
|
cert_path
|
||||||
|
and os.path.isfile(cert_path)
|
||||||
|
and user_sslopt.get("ca_certs", None) is None
|
||||||
|
):
|
||||||
|
sslopt["ca_certs"] = cert_path
|
||||||
|
elif (
|
||||||
|
cert_path
|
||||||
|
and os.path.isdir(cert_path)
|
||||||
|
and user_sslopt.get("ca_cert_path", None) is None
|
||||||
|
):
|
||||||
|
sslopt["ca_cert_path"] = cert_path
|
||||||
|
|
||||||
|
if sslopt.get("server_hostname", None):
|
||||||
|
hostname = sslopt["server_hostname"]
|
||||||
|
|
||||||
|
check_hostname = sslopt.get("check_hostname", True)
|
||||||
|
sock = _wrap_sni_socket(sock, sslopt, hostname, check_hostname)
|
||||||
|
|
||||||
|
return sock
|
||||||
|
|
||||||
|
|
||||||
|
def _tunnel(sock: socket.socket, host, port: int, auth) -> socket.socket:
|
||||||
|
debug("Connecting proxy...")
|
||||||
|
connect_header = f"CONNECT {host}:{port} HTTP/1.1\r\n"
|
||||||
|
connect_header += f"Host: {host}:{port}\r\n"
|
||||||
|
|
||||||
|
# TODO: support digest auth.
|
||||||
|
if auth and auth[0]:
|
||||||
|
auth_str = auth[0]
|
||||||
|
if auth[1]:
|
||||||
|
auth_str += f":{auth[1]}"
|
||||||
|
encoded_str = base64encode(auth_str.encode()).strip().decode().replace("\n", "")
|
||||||
|
connect_header += f"Proxy-Authorization: Basic {encoded_str}\r\n"
|
||||||
|
connect_header += "\r\n"
|
||||||
|
dump("request header", connect_header)
|
||||||
|
|
||||||
|
send(sock, connect_header)
|
||||||
|
|
||||||
|
try:
|
||||||
|
status, _, _ = read_headers(sock)
|
||||||
|
except Exception as e:
|
||||||
|
raise WebSocketProxyException(str(e))
|
||||||
|
|
||||||
|
if status != 200:
|
||||||
|
raise WebSocketProxyException(f"failed CONNECT via proxy status: {status}")
|
||||||
|
|
||||||
|
return sock
|
||||||
|
|
||||||
|
|
||||||
|
def read_headers(sock: socket.socket) -> tuple:
|
||||||
|
status = None
|
||||||
|
status_message = None
|
||||||
|
headers: dict = {}
|
||||||
|
trace("--- response header ---")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
line = recv_line(sock)
|
||||||
|
line = line.decode("utf-8").strip()
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
trace(line)
|
||||||
|
if not status:
|
||||||
|
status_info = line.split(" ", 2)
|
||||||
|
status = int(status_info[1])
|
||||||
|
if len(status_info) > 2:
|
||||||
|
status_message = status_info[2]
|
||||||
|
else:
|
||||||
|
kv = line.split(":", 1)
|
||||||
|
if len(kv) != 2:
|
||||||
|
raise WebSocketException("Invalid header")
|
||||||
|
key, value = kv
|
||||||
|
if key.lower() == "set-cookie" and headers.get("set-cookie"):
|
||||||
|
headers["set-cookie"] = headers.get("set-cookie") + "; " + value.strip()
|
||||||
|
else:
|
||||||
|
headers[key.lower()] = value.strip()
|
||||||
|
|
||||||
|
trace("-----------------------")
|
||||||
|
|
||||||
|
return status, headers, status_message
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
"""
|
||||||
|
_logging.py
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright 2024 engn33r
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_logger = logging.getLogger("websocket")
|
||||||
|
try:
|
||||||
|
from logging import NullHandler
|
||||||
|
except ImportError:
|
||||||
|
|
||||||
|
class NullHandler(logging.Handler):
|
||||||
|
def emit(self, record) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
_logger.addHandler(NullHandler())
|
||||||
|
|
||||||
|
_traceEnabled = False
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"enableTrace",
|
||||||
|
"dump",
|
||||||
|
"error",
|
||||||
|
"warning",
|
||||||
|
"debug",
|
||||||
|
"trace",
|
||||||
|
"isEnabledForError",
|
||||||
|
"isEnabledForDebug",
|
||||||
|
"isEnabledForTrace",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def enableTrace(
|
||||||
|
traceable: bool,
|
||||||
|
handler: logging.StreamHandler = logging.StreamHandler(),
|
||||||
|
level: str = "DEBUG",
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Turn on/off the traceability.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
traceable: bool
|
||||||
|
If set to True, traceability is enabled.
|
||||||
|
"""
|
||||||
|
global _traceEnabled
|
||||||
|
_traceEnabled = traceable
|
||||||
|
if traceable:
|
||||||
|
_logger.addHandler(handler)
|
||||||
|
_logger.setLevel(getattr(logging, level))
|
||||||
|
|
||||||
|
|
||||||
|
def dump(title: str, message: str) -> None:
|
||||||
|
if _traceEnabled:
|
||||||
|
_logger.debug(f"--- {title} ---")
|
||||||
|
_logger.debug(message)
|
||||||
|
_logger.debug("-----------------------")
|
||||||
|
|
||||||
|
|
||||||
|
def error(msg: str) -> None:
|
||||||
|
_logger.error(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def warning(msg: str) -> None:
|
||||||
|
_logger.warning(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def debug(msg: str) -> None:
|
||||||
|
_logger.debug(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def info(msg: str) -> None:
|
||||||
|
_logger.info(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def trace(msg: str) -> None:
|
||||||
|
if _traceEnabled:
|
||||||
|
_logger.debug(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def isEnabledForError() -> bool:
|
||||||
|
return _logger.isEnabledFor(logging.ERROR)
|
||||||
|
|
||||||
|
|
||||||
|
def isEnabledForDebug() -> bool:
|
||||||
|
return _logger.isEnabledFor(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
def isEnabledForTrace() -> bool:
|
||||||
|
return _traceEnabled
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
import errno
|
||||||
|
import selectors
|
||||||
|
import socket
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from ._exceptions import (
|
||||||
|
WebSocketConnectionClosedException,
|
||||||
|
WebSocketTimeoutException,
|
||||||
|
)
|
||||||
|
from ._ssl_compat import SSLError, SSLWantReadError, SSLWantWriteError
|
||||||
|
from ._utils import extract_error_code, extract_err_message
|
||||||
|
|
||||||
|
"""
|
||||||
|
_socket.py
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright 2024 engn33r
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
DEFAULT_SOCKET_OPTION = [(socket.SOL_TCP, socket.TCP_NODELAY, 1)]
|
||||||
|
if hasattr(socket, "SO_KEEPALIVE"):
|
||||||
|
DEFAULT_SOCKET_OPTION.append((socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1))
|
||||||
|
if hasattr(socket, "TCP_KEEPIDLE"):
|
||||||
|
DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPIDLE, 30))
|
||||||
|
if hasattr(socket, "TCP_KEEPINTVL"):
|
||||||
|
DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPINTVL, 10))
|
||||||
|
if hasattr(socket, "TCP_KEEPCNT"):
|
||||||
|
DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPCNT, 3))
|
||||||
|
|
||||||
|
_default_timeout = None
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"DEFAULT_SOCKET_OPTION",
|
||||||
|
"sock_opt",
|
||||||
|
"setdefaulttimeout",
|
||||||
|
"getdefaulttimeout",
|
||||||
|
"recv",
|
||||||
|
"recv_line",
|
||||||
|
"send",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class sock_opt:
|
||||||
|
def __init__(self, sockopt: list, sslopt: dict) -> None:
|
||||||
|
if sockopt is None:
|
||||||
|
sockopt = []
|
||||||
|
if sslopt is None:
|
||||||
|
sslopt = {}
|
||||||
|
self.sockopt = sockopt
|
||||||
|
self.sslopt = sslopt
|
||||||
|
self.timeout = None
|
||||||
|
|
||||||
|
|
||||||
|
def setdefaulttimeout(timeout: Union[int, float, None]) -> None:
|
||||||
|
"""
|
||||||
|
Set the global timeout setting to connect.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
timeout: int or float
|
||||||
|
default socket timeout time (in seconds)
|
||||||
|
"""
|
||||||
|
global _default_timeout
|
||||||
|
_default_timeout = timeout
|
||||||
|
|
||||||
|
|
||||||
|
def getdefaulttimeout() -> Union[int, float, None]:
|
||||||
|
"""
|
||||||
|
Get default timeout
|
||||||
|
|
||||||
|
Returns
|
||||||
|
----------
|
||||||
|
_default_timeout: int or float
|
||||||
|
Return the global timeout setting (in seconds) to connect.
|
||||||
|
"""
|
||||||
|
return _default_timeout
|
||||||
|
|
||||||
|
|
||||||
|
def recv(sock: socket.socket, bufsize: int) -> bytes:
|
||||||
|
if not sock:
|
||||||
|
raise WebSocketConnectionClosedException("socket is already closed.")
|
||||||
|
|
||||||
|
def _recv():
|
||||||
|
try:
|
||||||
|
return sock.recv(bufsize)
|
||||||
|
except SSLWantReadError:
|
||||||
|
pass
|
||||||
|
except socket.error as exc:
|
||||||
|
error_code = extract_error_code(exc)
|
||||||
|
if error_code not in [errno.EAGAIN, errno.EWOULDBLOCK]:
|
||||||
|
raise
|
||||||
|
|
||||||
|
sel = selectors.DefaultSelector()
|
||||||
|
sel.register(sock, selectors.EVENT_READ)
|
||||||
|
|
||||||
|
r = sel.select(sock.gettimeout())
|
||||||
|
sel.close()
|
||||||
|
|
||||||
|
if r:
|
||||||
|
return sock.recv(bufsize)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if sock.gettimeout() == 0:
|
||||||
|
bytes_ = sock.recv(bufsize)
|
||||||
|
else:
|
||||||
|
bytes_ = _recv()
|
||||||
|
except TimeoutError:
|
||||||
|
raise WebSocketTimeoutException("Connection timed out")
|
||||||
|
except socket.timeout as e:
|
||||||
|
message = extract_err_message(e)
|
||||||
|
raise WebSocketTimeoutException(message)
|
||||||
|
except SSLError as e:
|
||||||
|
message = extract_err_message(e)
|
||||||
|
if isinstance(message, str) and "timed out" in message:
|
||||||
|
raise WebSocketTimeoutException(message)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
if not bytes_:
|
||||||
|
raise WebSocketConnectionClosedException("Connection to remote host was lost.")
|
||||||
|
|
||||||
|
return bytes_
|
||||||
|
|
||||||
|
|
||||||
|
def recv_line(sock: socket.socket) -> bytes:
|
||||||
|
line = []
|
||||||
|
while True:
|
||||||
|
c = recv(sock, 1)
|
||||||
|
line.append(c)
|
||||||
|
if c == b"\n":
|
||||||
|
break
|
||||||
|
return b"".join(line)
|
||||||
|
|
||||||
|
|
||||||
|
def send(sock: socket.socket, data: Union[bytes, str]) -> int:
|
||||||
|
if isinstance(data, str):
|
||||||
|
data = data.encode("utf-8")
|
||||||
|
|
||||||
|
if not sock:
|
||||||
|
raise WebSocketConnectionClosedException("socket is already closed.")
|
||||||
|
|
||||||
|
def _send():
|
||||||
|
try:
|
||||||
|
return sock.send(data)
|
||||||
|
except SSLWantWriteError:
|
||||||
|
pass
|
||||||
|
except socket.error as exc:
|
||||||
|
error_code = extract_error_code(exc)
|
||||||
|
if error_code is None:
|
||||||
|
raise
|
||||||
|
if error_code not in [errno.EAGAIN, errno.EWOULDBLOCK]:
|
||||||
|
raise
|
||||||
|
|
||||||
|
sel = selectors.DefaultSelector()
|
||||||
|
sel.register(sock, selectors.EVENT_WRITE)
|
||||||
|
|
||||||
|
w = sel.select(sock.gettimeout())
|
||||||
|
sel.close()
|
||||||
|
|
||||||
|
if w:
|
||||||
|
return sock.send(data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if sock.gettimeout() == 0:
|
||||||
|
return sock.send(data)
|
||||||
|
else:
|
||||||
|
return _send()
|
||||||
|
except socket.timeout as e:
|
||||||
|
message = extract_err_message(e)
|
||||||
|
raise WebSocketTimeoutException(message)
|
||||||
|
except Exception as e:
|
||||||
|
message = extract_err_message(e)
|
||||||
|
if isinstance(message, str) and "timed out" in message:
|
||||||
|
raise WebSocketTimeoutException(message)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
"""
|
||||||
|
_ssl_compat.py
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright 2024 engn33r
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"HAVE_SSL",
|
||||||
|
"ssl",
|
||||||
|
"SSLError",
|
||||||
|
"SSLEOFError",
|
||||||
|
"SSLWantReadError",
|
||||||
|
"SSLWantWriteError",
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
import ssl
|
||||||
|
from ssl import SSLError, SSLEOFError, SSLWantReadError, SSLWantWriteError
|
||||||
|
|
||||||
|
HAVE_SSL = True
|
||||||
|
except ImportError:
|
||||||
|
# dummy class of SSLError for environment without ssl support
|
||||||
|
class SSLError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class SSLEOFError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class SSLWantReadError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class SSLWantWriteError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
ssl = None
|
||||||
|
HAVE_SSL = False
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
from typing import Optional
|
||||||
|
from urllib.parse import unquote, urlparse
|
||||||
|
from ._exceptions import WebSocketProxyException
|
||||||
|
|
||||||
|
"""
|
||||||
|
_url.py
|
||||||
|
websocket - WebSocket client library for Python
|
||||||
|
|
||||||
|
Copyright 2024 engn33r
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__all__ = ["parse_url", "get_proxy_info"]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_url(url: str) -> tuple:
|
||||||
|
"""
|
||||||
|
parse url and the result is tuple of
|
||||||
|
(hostname, port, resource path and the flag of secure mode)
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
url: str
|
||||||
|
url string.
|
||||||
|
"""
|
||||||
|
if ":" not in url:
|
||||||
|
raise ValueError("url is invalid")
|
||||||
|
|
||||||
|
scheme, url = url.split(":", 1)
|
||||||
|
|
||||||
|
parsed = urlparse(url, scheme="http")
|
||||||
|
if parsed.hostname:
|
||||||
|
hostname = parsed.hostname
|
||||||
|
else:
|
||||||
|
raise ValueError("hostname is invalid")
|
||||||
|
port = 0
|
||||||
|
if parsed.port:
|
||||||
|
port = parsed.port
|
||||||
|
|
||||||
|
is_secure = False
|
||||||
|
if scheme == "ws":
|
||||||
|
if not port:
|
||||||
|
port = 80
|
||||||
|
elif scheme == "wss":
|
||||||
|
is_secure = True
|
||||||
|
if not port:
|
||||||
|
port = 443
|
||||||
|
else:
|
||||||
|
raise ValueError("scheme %s is invalid" % scheme)
|
||||||
|
|
||||||
|
if parsed.path:
|
||||||
|
resource = parsed.path
|
||||||
|
else:
|
||||||
|
resource = "/"
|
||||||
|
|
||||||
|
if parsed.query:
|
||||||
|
resource += f"?{parsed.query}"
|
||||||
|
|
||||||
|
return hostname, port, resource, is_secure
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_NO_PROXY_HOST = ["localhost", "127.0.0.1"]
|
||||||
|
|
||||||
|
|
||||||
|
def _is_ip_address(addr: str) -> bool:
|
||||||
|
try:
|
||||||
|
socket.inet_aton(addr)
|
||||||
|
except socket.error:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _is_subnet_address(hostname: str) -> bool:
|
||||||
|
try:
|
||||||
|
addr, netmask = hostname.split("/")
|
||||||
|
return _is_ip_address(addr) and 0 <= int(netmask) < 32
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _is_address_in_network(ip: str, net: str) -> bool:
|
||||||
|
ipaddr: int = struct.unpack("!I", socket.inet_aton(ip))[0]
|
||||||
|
netaddr, netmask = net.split("/")
|
||||||
|
netaddr: int = struct.unpack("!I", socket.inet_aton(netaddr))[0]
|
||||||
|
|
||||||
|
netmask = (0xFFFFFFFF << (32 - int(netmask))) & 0xFFFFFFFF
|
||||||
|
return ipaddr & netmask == netaddr
|
||||||
|
|
||||||
|
|
||||||
|
def _is_no_proxy_host(hostname: str, no_proxy: Optional[list]) -> bool:
|
||||||
|
if not no_proxy:
|
||||||
|
if v := os.environ.get("no_proxy", os.environ.get("NO_PROXY", "")).replace(
|
||||||
|
" ", ""
|
||||||
|
):
|
||||||
|
no_proxy = v.split(",")
|
||||||
|
if not no_proxy:
|
||||||
|
no_proxy = DEFAULT_NO_PROXY_HOST
|
||||||
|
|
||||||
|
if "*" in no_proxy:
|
||||||
|
return True
|
||||||
|
if hostname in no_proxy:
|
||||||
|
return True
|
||||||
|
if _is_ip_address(hostname):
|
||||||
|
return any(
|
||||||
|
[
|
||||||
|
_is_address_in_network(hostname, subnet)
|
||||||
|
for subnet in no_proxy
|
||||||
|
if _is_subnet_address(subnet)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
for domain in [domain for domain in no_proxy if domain.startswith(".")]:
|
||||||
|
if hostname.endswith(domain):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_proxy_info(
|
||||||
|
hostname: str,
|
||||||
|
is_secure: bool,
|
||||||
|
proxy_host: Optional[str] = None,
|
||||||
|
proxy_port: int = 0,
|
||||||
|
proxy_auth: Optional[tuple] = None,
|
||||||
|
no_proxy: Optional[list] = None,
|
||||||
|
proxy_type: str = "http",
|
||||||
|
) -> tuple:
|
||||||
|
"""
|
||||||
|
Try to retrieve proxy host and port from environment
|
||||||
|
if not provided in options.
|
||||||
|
Result is (proxy_host, proxy_port, proxy_auth).
|
||||||
|
proxy_auth is tuple of username and password
|
||||||
|
of proxy authentication information.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
hostname: str
|
||||||
|
Websocket server name.
|
||||||
|
is_secure: bool
|
||||||
|
Is the connection secure? (wss) looks for "https_proxy" in env
|
||||||
|
instead of "http_proxy"
|
||||||
|
proxy_host: str
|
||||||
|
http proxy host name.
|
||||||
|
proxy_port: str or int
|
||||||
|
http proxy port.
|
||||||
|
no_proxy: list
|
||||||
|
Whitelisted host names that don't use the proxy.
|
||||||
|
proxy_auth: tuple
|
||||||
|
HTTP proxy auth information. Tuple of username and password. Default is None.
|
||||||
|
proxy_type: str
|
||||||
|
Specify the proxy protocol (http, socks4, socks4a, socks5, socks5h). Default is "http".
|
||||||
|
Use socks4a or socks5h if you want to send DNS requests through the proxy.
|
||||||
|
"""
|
||||||
|
if _is_no_proxy_host(hostname, no_proxy):
|
||||||
|
return None, 0, None
|
||||||
|
|
||||||
|
if proxy_host:
|
||||||
|
if not proxy_port:
|
||||||
|
raise WebSocketProxyException("Cannot use port 0 when proxy_host specified")
|
||||||
|
port = proxy_port
|
||||||
|
auth = proxy_auth
|
||||||
|
return proxy_host, port, auth
|
||||||
|
|
||||||
|
env_key = "https_proxy" if is_secure else "http_proxy"
|
||||||
|
value = os.environ.get(env_key, os.environ.get(env_key.upper(), "")).replace(
|
||||||
|
" ", ""
|
||||||
|
)
|
||||||
|
if value:
|
||||||
|
proxy = urlparse(value)
|
||||||
|
auth = (
|
||||||
|
(unquote(proxy.username), unquote(proxy.password))
|
||||||
|
if proxy.username
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
return proxy.hostname, proxy.port, auth
|
||||||
|
|
||||||
|
return None, 0, None
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user