└── addons └── copilot ├── plugin.cfg ├── Plugin.gd ├── LLM.gd ├── large_icon.tres ├── small_icon.tres ├── OpenAICompletion.gd ├── UUID.gd ├── OpenAIChat.gd ├── GithubCopilot.gd ├── CopilotUI.tscn └── Copilot.gd /addons/copilot/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Copilot" 4 | description="Use large language models for AI assisted development in the Godot Engine." 5 | author="Markus Sobkowski" 6 | version="1.0" 7 | script="Plugin.gd" 8 | -------------------------------------------------------------------------------- /addons/copilot/Plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | const version = "1.0.0" 5 | const scene_path = "res://addons/copilot/CopilotUI.tscn" 6 | 7 | var dock 8 | var editor_interface = get_editor_interface() 9 | 10 | func _enter_tree() -> void: 11 | if(!dock): 12 | dock = load(scene_path).instantiate() 13 | add_control_to_dock(EditorPlugin.DOCK_SLOT_RIGHT_UL, dock) 14 | main_screen_changed.connect(Callable(dock, "on_main_screen_changed")) 15 | dock.editor_interface = get_editor_interface() 16 | dock.set_version(version) 17 | 18 | func _exit_tree(): 19 | remove_control_from_docks(dock) 20 | dock.queue_free() 21 | 22 | -------------------------------------------------------------------------------- /addons/copilot/LLM.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Node 3 | 4 | var model 5 | var api_key 6 | var allow_multiline 7 | 8 | signal completion_received(completion, pre, post) 9 | signal completion_error(error) 10 | 11 | #Expects return value of String Array 12 | func _get_models(): 13 | return [] 14 | 15 | #Sets active model 16 | func _set_model(model_name): 17 | model = model_name 18 | 19 | #Sets API key 20 | func _set_api_key(key): 21 | api_key = key 22 | 23 | #Determines if multiline completions are allowed 24 | func _set_multiline(allowed): 25 | allow_multiline = allowed 26 | 27 | #Sends user prompt 28 | func _send_user_prompt(user_prompt, user_suffix): 29 | pass 30 | 31 | -------------------------------------------------------------------------------- /addons/copilot/large_icon.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="ShaderMaterial" load_steps=2 format=3 uid="uid://bl1rtf743e4l3"] 2 | 3 | [sub_resource type="Shader" id="9"] 4 | code = "shader_type canvas_item; 5 | 6 | uniform vec4 circle_color : source_color = vec4(0.0, 1.0, 1.0, 1.0); 7 | uniform float circle_speed : hint_range(0.0, 10.0) = 1.0; 8 | uniform float circle_width : hint_range(0.0, 1.0) = 0.1; 9 | uniform float circle_count : hint_range(1.0, 20.0) = 6.0; 10 | uniform float circle_size : hint_range(0.1, 2.0) = 0.5; 11 | 12 | // Glow settings 13 | uniform float glow_strength : hint_range(0.0, 1.0) = 0.5; 14 | uniform float glow_radius : hint_range(0.0, 1.0) = 0.2; 15 | 16 | void fragment() { 17 | vec2 uv = UV * 3.0 - vec2(1.5, 1.5); 18 | float len = length(uv); 19 | 20 | float circle = 0.0; 21 | for (float i = 0.0; i < circle_count; i++) { 22 | float t = i / circle_count; 23 | float time_offset = t * 6.28318; // 2 * PI 24 | float radius = (1.0 - t * circle_size) * (1.0 + sin(TIME * circle_speed + time_offset) * 0.1); 25 | float circle_strength = smoothstep(radius - circle_width, radius, len) - smoothstep(radius, radius + circle_width, len); 26 | circle = max(circle, circle_strength); 27 | } 28 | 29 | // Glow effect 30 | float glow = smoothstep(circle_width, circle_width + glow_radius, circle); 31 | circle += glow_strength * glow; 32 | 33 | vec4 col = vec4(circle_color.rgb * circle, circle_color.a * circle); 34 | COLOR = col; 35 | }" 36 | 37 | [resource] 38 | shader = SubResource("9") 39 | shader_parameter/circle_color = Color(0.533, 0.60475, 0.82, 1) 40 | shader_parameter/circle_speed = 2.881 41 | shader_parameter/circle_width = 0.05 42 | shader_parameter/circle_count = 4.0 43 | shader_parameter/circle_size = 0.8 44 | shader_parameter/glow_strength = 0.4 45 | shader_parameter/glow_radius = 0.0 46 | -------------------------------------------------------------------------------- /addons/copilot/small_icon.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="ShaderMaterial" load_steps=2 format=3 uid="uid://cccmbprav6vgu"] 2 | 3 | [sub_resource type="Shader" id="9"] 4 | code = "shader_type canvas_item; 5 | 6 | uniform vec4 circle_color : source_color = vec4(0.0, 1.0, 1.0, 1.0); 7 | uniform float circle_speed : hint_range(0.0, 10.0) = 1.0; 8 | uniform float circle_width : hint_range(0.0, 1.0) = 0.1; 9 | uniform float circle_count : hint_range(1.0, 20.0) = 6.0; 10 | uniform float circle_size : hint_range(0.1, 2.0) = 0.5; 11 | 12 | // Glow settings 13 | uniform float glow_strength : hint_range(0.0, 1.0) = 0.5; 14 | uniform float glow_radius : hint_range(0.0, 1.0) = 0.2; 15 | 16 | void fragment() { 17 | vec2 uv = UV * 3.0 - vec2(1.5, 1.5); 18 | float len = length(uv); 19 | 20 | float circle = 0.0; 21 | for (float i = 0.0; i < circle_count; i++) { 22 | float t = i / circle_count; 23 | float time_offset = t * 6.28318; // 2 * PI 24 | float radius = (1.0 - t * circle_size) * (1.0 + sin(TIME * circle_speed + time_offset) * 0.1); 25 | float circle_strength = smoothstep(radius - circle_width, radius, len) - smoothstep(radius, radius + circle_width, len); 26 | circle = max(circle, circle_strength); 27 | } 28 | 29 | // Glow effect 30 | float glow = smoothstep(circle_width, circle_width + glow_radius, circle); 31 | circle += glow_strength * glow; 32 | 33 | vec4 col = vec4(circle_color.rgb * circle, circle_color.a * circle); 34 | COLOR = col; 35 | }" 36 | 37 | [resource] 38 | shader = SubResource("9") 39 | shader_parameter/circle_color = Color(0.533, 0.60475, 0.82, 1) 40 | shader_parameter/circle_speed = 4.0 41 | shader_parameter/circle_width = 0.3 42 | shader_parameter/circle_count = 2.0 43 | shader_parameter/circle_size = 0.8 44 | shader_parameter/glow_strength = 1.0 45 | shader_parameter/glow_radius = 0.05 46 | -------------------------------------------------------------------------------- /addons/copilot/OpenAICompletion.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "res://addons/copilot/LLM.gd" 3 | 4 | const URL = "https://api.openai.com/v1/completions" 5 | const PROMPT_PREFIX = """#This is a GDScript script using Godot 4.0. 6 | #That means the new GDScript 2.0 syntax is used. Here's a couple of important changes that were introduced: 7 | #- Use @export annotation for exports 8 | #- Use Node3D instead of Spatial, and position instead of translation 9 | #- Use randf_range and randi_range instead of rand_range 10 | #- Connect signals via node.SIGNAL_NAME.connect(Callable(TARGET_OBJECT, TARGET_FUNC)) 11 | #- Connect signals via node.SIGNAL_NAME.connect(Callable(TARGET_OBJECT, TARGET_FUNC)) 12 | #- Use rad_to_deg instead of rad2deg 13 | #- Use PackedByteArray instead of PoolByteArray 14 | #- Use instantiate instead of instance 15 | #- You can't use enumerate(OBJECT). Instead, use "for i in len(OBJECT):" 16 | # 17 | #Remember, this is not Python. It's GDScript for use in Godot. 18 | 19 | 20 | """ 21 | const MAX_LENGTH = 8500 22 | 23 | func _get_models(): 24 | return [ 25 | "text-davinci-003" 26 | ] 27 | 28 | func _set_model(model_name): 29 | model = model_name 30 | 31 | func _send_user_prompt(user_prompt, user_suffix): 32 | get_completion(user_prompt, user_suffix) 33 | 34 | func get_completion(_prompt, _suffix): 35 | var prompt = _prompt 36 | var suffix = _suffix 37 | var combined_prompt = prompt + suffix 38 | var diff = combined_prompt.length() - MAX_LENGTH 39 | if diff > 0: 40 | if suffix.length() > diff: 41 | suffix = suffix.substr(0,diff) 42 | else: 43 | prompt = prompt.substr(diff - suffix.length()) 44 | suffix = "" 45 | var body = { 46 | "model": model, 47 | "prompt": PROMPT_PREFIX + prompt, 48 | "suffix": suffix, 49 | "temperature": 0.7, 50 | "max_tokens": 500, 51 | "stop": "\n\n" if allow_multiline else "\n" 52 | } 53 | var headers = [ 54 | "Content-Type: application/json", 55 | "Authorization: Bearer %s" % api_key 56 | ] 57 | var http_request = HTTPRequest.new() 58 | add_child(http_request) 59 | http_request.connect("request_completed",on_request_completed.bind(prompt, suffix, http_request)) 60 | var json_body = JSON.stringify(body) 61 | var error = http_request.request(URL, headers, HTTPClient.METHOD_POST, json_body) 62 | if error != OK: 63 | emit_signal("completion_error", null) 64 | 65 | func on_request_completed(result, response_code, headers, body, pre, post, http_request): 66 | var test_json_conv = JSON.new() 67 | test_json_conv.parse(body.get_string_from_utf8()) 68 | var json = test_json_conv.get_data() 69 | var response = json 70 | if !response.has("choices"): 71 | emit_signal("completion_error", response) 72 | return 73 | var completion = response.choices[0].text 74 | if is_instance_valid(http_request): 75 | http_request.queue_free() 76 | emit_signal("completion_received", completion, pre, post) 77 | -------------------------------------------------------------------------------- /addons/copilot/UUID.gd: -------------------------------------------------------------------------------- 1 | # From: https://github.com/binogure-studio/godot-uuid 2 | # Credit: binogure-studio 3 | 4 | # Note: The code might not be as pretty it could be, since it's written 5 | # in a way that maximizes performance. Methods are inlined and loops are avoided. 6 | extends Node 7 | class_name UUID 8 | 9 | const BYTE_MASK: int = 0b11111111 10 | 11 | static func uuidbin(): 12 | # 16 random bytes with the bytes on index 6 and 8 modified 13 | return [ 14 | randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, 15 | randi() & BYTE_MASK, randi() & BYTE_MASK, ((randi() & BYTE_MASK) & 0x0f) | 0x40, randi() & BYTE_MASK, 16 | ((randi() & BYTE_MASK) & 0x3f) | 0x80, randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, 17 | randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, 18 | ] 19 | 20 | static func uuidbinrng(rng: RandomNumberGenerator): 21 | return [ 22 | rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, 23 | rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, ((rng.randi() & BYTE_MASK) & 0x0f) | 0x40, rng.randi() & BYTE_MASK, 24 | ((rng.randi() & BYTE_MASK) & 0x3f) | 0x80, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, 25 | rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, rng.randi() & BYTE_MASK, 26 | ] 27 | 28 | static func v4(): 29 | # 16 random bytes with the bytes on index 6 and 8 modified 30 | var b = uuidbin() 31 | 32 | return '%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x' % [ 33 | # low 34 | b[0], b[1], b[2], b[3], 35 | 36 | # mid 37 | b[4], b[5], 38 | 39 | # hi 40 | b[6], b[7], 41 | 42 | # clock 43 | b[8], b[9], 44 | 45 | # clock 46 | b[10], b[11], b[12], b[13], b[14], b[15] 47 | ] 48 | 49 | static func v4_rng(rng: RandomNumberGenerator): 50 | # 16 random bytes with the bytes on index 6 and 8 modified 51 | var b = uuidbinrng(rng) 52 | 53 | return '%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x' % [ 54 | # low 55 | b[0], b[1], b[2], b[3], 56 | 57 | # mid 58 | b[4], b[5], 59 | 60 | # hi 61 | b[6], b[7], 62 | 63 | # clock 64 | b[8], b[9], 65 | 66 | # clock 67 | b[10], b[11], b[12], b[13], b[14], b[15] 68 | ] 69 | 70 | var _uuid: Array 71 | 72 | func _init(rng := RandomNumberGenerator.new()) -> void: 73 | _uuid = uuidbinrng(rng) 74 | 75 | func as_array() -> Array: 76 | return _uuid.duplicate() 77 | 78 | func as_dict(big_endian := true) -> Dictionary: 79 | if big_endian: 80 | return { 81 | "low" : (_uuid[0] << 24) + (_uuid[1] << 16) + (_uuid[2] << 8 ) + _uuid[3], 82 | "mid" : (_uuid[4] << 8 ) + _uuid[5], 83 | "hi" : (_uuid[6] << 8 ) + _uuid[7], 84 | "clock": (_uuid[8] << 8 ) + _uuid[9], 85 | "node" : (_uuid[10] << 40) + (_uuid[11] << 32) + (_uuid[12] << 24) + (_uuid[13] << 16) + (_uuid[14] << 8 ) + _uuid[15] 86 | } 87 | else: 88 | return { 89 | "low" : _uuid[0] + (_uuid[1] << 8 ) + (_uuid[2] << 16) + (_uuid[3] << 24), 90 | "mid" : _uuid[4] + (_uuid[5] << 8 ), 91 | "hi" : _uuid[6] + (_uuid[7] << 8 ), 92 | "clock": _uuid[8] + (_uuid[9] << 8 ), 93 | "node" : _uuid[10] + (_uuid[11] << 8 ) + (_uuid[12] << 16) + (_uuid[13] << 24) + (_uuid[14] << 32) + (_uuid[15] << 40) 94 | } 95 | 96 | func as_string() -> String: 97 | return '%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x' % [ 98 | # low 99 | _uuid[0], _uuid[1], _uuid[2], _uuid[3], 100 | 101 | # mid 102 | _uuid[4], _uuid[5], 103 | 104 | # hi 105 | _uuid[6], _uuid[7], 106 | 107 | # clock 108 | _uuid[8], _uuid[9], 109 | 110 | # node 111 | _uuid[10], _uuid[11], _uuid[12], _uuid[13], _uuid[14], _uuid[15] 112 | ] 113 | 114 | func is_equal(other) -> bool: 115 | # Godot Engine compares Array recursively 116 | # There's no need for custom comparison here. 117 | return _uuid == other._uuid 118 | -------------------------------------------------------------------------------- /addons/copilot/OpenAIChat.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "res://addons/copilot/LLM.gd" 3 | 4 | const URL = "https://api.openai.com/v1/chat/completions" 5 | const SYSTEM_TEMPLATE = """You are a brilliant coding assistant for the game-engine Godot. The version used is Godot 4.0, and all code must be valid GDScript! 6 | That means the new GDScript 2.0 syntax is used. Here's a couple of important changes that were introduced: 7 | - Use @export annotation for exports 8 | - Use Node3D instead of Spatial, and position instead of translation 9 | - Use randf_range and randi_range instead of rand_range 10 | - Connect signals via node.SIGNAL_NAME.connect(Callable(TARGET_OBJECT, TARGET_FUNC)) 11 | - Same for sort_custom calls, pass a Callable(TARGET_OBJECT, TARGET_FUNC) 12 | - Use rad_to_deg instead of rad2deg 13 | - Use PackedByteArray instead of PoolByteArray 14 | - Use instantiate instead of instance 15 | - You can't use enumerate(OBJECT). Instead, use "for i in len(OBJECT):" 16 | 17 | Remember, this is not Python. It's GDScript for use in Godot. 18 | 19 | You may only answer in code, never add any explanations. In your prompt, there will be an !INSERT_CODE_HERE! tag. Only respond with plausible code that may be inserted at that point. Never repeat the full script, only the parts to be inserted. Treat this as if it was an autocompletion. You may continue whatever word or expression was left unfinished before the tag. Make sure indentation matches the surrounding context.""" 20 | const INSERT_TAG = "!INSERT_CODE_HERE!" 21 | const MAX_LENGTH = 8500 22 | 23 | class Message: 24 | var role: String 25 | var content: String 26 | 27 | func get_json(): 28 | return { 29 | "role": role, 30 | "content": content 31 | } 32 | 33 | const ROLES = { 34 | "SYSTEM": "system", 35 | "USER": "user", 36 | "ASSISTANT": "assistant" 37 | } 38 | 39 | func _get_models(): 40 | return [ 41 | "gpt-3.5-turbo", 42 | "gpt-4" 43 | ] 44 | 45 | func _set_model(model_name): 46 | model = model_name 47 | 48 | func _send_user_prompt(user_prompt, user_suffix): 49 | var messages = format_prompt(user_prompt, user_suffix) 50 | get_completion(messages, user_prompt, user_suffix) 51 | 52 | func format_prompt(prompt, suffix): 53 | var messages = [] 54 | var system_prompt = SYSTEM_TEMPLATE 55 | 56 | var combined_prompt = prompt + suffix 57 | var diff = combined_prompt.length() - MAX_LENGTH 58 | if diff > 0: 59 | if suffix.length() > diff: 60 | suffix = suffix.substr(0,diff) 61 | else: 62 | prompt = prompt.substr(diff - suffix.length()) 63 | suffix = "" 64 | var user_prompt = prompt + INSERT_TAG + suffix 65 | 66 | var msg = Message.new() 67 | msg.role = ROLES.SYSTEM 68 | msg.content = system_prompt 69 | messages.append(msg.get_json()) 70 | 71 | msg = Message.new() 72 | msg.role = ROLES.USER 73 | msg.content = user_prompt 74 | messages.append(msg.get_json()) 75 | 76 | return messages 77 | 78 | func get_completion(messages, prompt, suffix): 79 | var body = { 80 | "model": model, 81 | "messages": messages, 82 | "temperature": 0.7, 83 | "max_tokens": 500, 84 | "stop": "\n\n" if allow_multiline else "\n" 85 | } 86 | var headers = [ 87 | "Content-Type: application/json", 88 | "Authorization: Bearer %s" % api_key 89 | ] 90 | var http_request = HTTPRequest.new() 91 | add_child(http_request) 92 | http_request.connect("request_completed",on_request_completed.bind(prompt, suffix, http_request)) 93 | var json_body = JSON.stringify(body) 94 | var error = http_request.request(URL, headers, HTTPClient.METHOD_POST, json_body) 95 | if error != OK: 96 | emit_signal("completion_error", null) 97 | 98 | 99 | func on_request_completed(result, response_code, headers, body, pre, post, http_request): 100 | var test_json_conv = JSON.new() 101 | test_json_conv.parse(body.get_string_from_utf8()) 102 | var json = test_json_conv.get_data() 103 | var response = json 104 | if !response.has("choices") : 105 | emit_signal("completion_error", response) 106 | return 107 | var completion = response.choices[0].message 108 | if is_instance_valid(http_request): 109 | http_request.queue_free() 110 | emit_signal("completion_received", completion.content, pre, post) 111 | -------------------------------------------------------------------------------- /addons/copilot/GithubCopilot.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "res://addons/copilot/LLM.gd" 3 | 4 | const URL = "https://api.githubcopilot.com/chat/completions" 5 | const AUTH_URL = "https://api.github.com/copilot_internal/v2/token" 6 | const SYSTEM_TEMPLATE = """You are a brilliant coding assistant for the game-engine Godot. The version used is Godot 4.0, and all code must be valid GDScript! 7 | That means the new GDScript 2.0 syntax is used. Here's a couple of important changes that were introduced: 8 | - Use @export annotation for exports 9 | - Use Node3D instead of Spatial, and position instead of translation 10 | - Use randf_range and randi_range instead of rand_range 11 | - Connect signals via node.SIGNAL_NAME.connect(Callable(TARGET_OBJECT, TARGET_FUNC)) 12 | - Same for sort_custom calls, pass a Callable(TARGET_OBJECT, TARGET_FUNC) 13 | - Use rad_to_deg instead of rad2deg 14 | - Use PackedByteArray instead of PoolByteArray 15 | - Use instantiate instead of instance 16 | - You can't use enumerate(OBJECT). Instead, use "for i in len(OBJECT):" 17 | 18 | Remember, this is not Python. It's GDScript for use in Godot. 19 | 20 | You may only answer in code, never add any explanations. In your prompt, there will be an !INSERT_CODE_HERE! tag. Only respond with plausible code that may be inserted at that point. Never repeat the full script, only the parts to be inserted. Treat this as if it was an autocompletion. You may continue whatever word or expression was left unfinished before the tag. Make sure indentation matches the surrounding context.""" 21 | const INSERT_TAG = "!INSERT_CODE_HERE!" 22 | const MAX_LENGTH = 8500 23 | 24 | const PREFERENCES_STORAGE_NAME = "user://github_copilot_llm.cfg" 25 | const PREFERENCES_PASS = "Jr55ICpdp3M3CuWHX0WHLqg3yh4XBjbXX" 26 | 27 | var machine_id 28 | var session_id 29 | var auth_token 30 | 31 | signal auth_token_retrieved 32 | 33 | class Message: 34 | var role: String 35 | var content: String 36 | 37 | func get_json(): 38 | return { 39 | "role": role, 40 | "content": content 41 | } 42 | 43 | const ROLES = { 44 | "SYSTEM": "system", 45 | "USER": "user", 46 | "ASSISTANT": "assistant" 47 | } 48 | 49 | func _get_models(): 50 | return [ 51 | "gpt-4-github-copilot" 52 | ] 53 | 54 | func _set_model(model_name): 55 | model = model_name.replace("github-copilot", "") 56 | 57 | func _send_user_prompt(user_prompt, user_suffix): 58 | var messages = format_prompt(user_prompt, user_suffix) 59 | get_completion(messages, user_prompt, user_suffix) 60 | 61 | func format_prompt(prompt, suffix): 62 | var messages = [] 63 | var system_prompt = SYSTEM_TEMPLATE 64 | 65 | var combined_prompt = prompt + suffix 66 | var diff = combined_prompt.length() - MAX_LENGTH 67 | if diff > 0: 68 | if suffix.length() > diff: 69 | suffix = suffix.substr(0,diff) 70 | else: 71 | prompt = prompt.substr(diff - suffix.length()) 72 | suffix = "" 73 | var user_prompt = prompt + INSERT_TAG + suffix 74 | 75 | var msg = Message.new() 76 | msg.role = ROLES.SYSTEM 77 | msg.content = system_prompt 78 | messages.append(msg.get_json()) 79 | 80 | msg = Message.new() 81 | msg.role = ROLES.USER 82 | msg.content = user_prompt 83 | messages.append(msg.get_json()) 84 | 85 | return messages 86 | 87 | func gen_hex_str(length: int) -> String: 88 | var rng = RandomNumberGenerator.new() 89 | var result = PackedByteArray() 90 | for i in range(length / 2): 91 | result.push_back(rng.randi_range(0, 255)) 92 | var hex_str = "" 93 | for byte in result: 94 | hex_str += "%02x" % byte 95 | return hex_str 96 | 97 | func create_headers(token: String, stream: bool): 98 | var contentType: String = "application/json; charset=utf-8" 99 | if stream: 100 | contentType = "text/event-stream; charset=utf-8" 101 | 102 | load_config() 103 | var uuidString: String = UUID.v4() 104 | 105 | return [ 106 | "Authorization: %s" % ("Bearer " + token), 107 | "X-Request-Id: %s" % uuidString, 108 | "Vscode-Sessionid: %s" % session_id, 109 | "Vscode-Machineid: %s" % machine_id, 110 | "Editor-Version: vscode/1.83.1", 111 | "Editor-Plugin-Version: copilot-chat/0.8.0", 112 | "Openai-Organization: github-copilot", 113 | "Openai-Intent: conversation-panel", 114 | "Content-Type: %s" % contentType, 115 | "User-Agent: GitHubCopilotChat/0.8.0", 116 | "Accept: */*", 117 | "Accept-Encoding: gzip,deflate,br", 118 | "Connection: close" 119 | ] 120 | 121 | func get_auth(): 122 | var headers = [ 123 | "Accept-Encoding: gzip", 124 | "Authorization: token %s" % api_key 125 | ] 126 | var http_request = HTTPRequest.new() 127 | add_child(http_request) 128 | http_request.connect("request_completed",on_auth_request_completed) 129 | var error = http_request.request(AUTH_URL, headers, HTTPClient.METHOD_GET) 130 | if error != OK: 131 | emit_signal("completion_error", null) 132 | 133 | func get_completion(messages, prompt, suffix): 134 | if not auth_token: 135 | get_auth() 136 | await auth_token_retrieved 137 | 138 | var body = { 139 | "model": model, 140 | "messages": messages, 141 | "temperature": 0.7, 142 | "top_p": 1, 143 | "n": 1, 144 | "stream": false, 145 | } 146 | var headers = create_headers(auth_token, false) 147 | var http_request = HTTPRequest.new() 148 | add_child(http_request) 149 | http_request.connect("request_completed",on_request_completed.bind(prompt, suffix, http_request)) 150 | var json_body = JSON.stringify(body) 151 | var error = http_request.request(URL, headers, HTTPClient.METHOD_POST, json_body) 152 | if error != OK: 153 | emit_signal("completion_error", null) 154 | 155 | func on_auth_request_completed(result, response_code, headers, body): 156 | var test_json_conv = JSON.new() 157 | test_json_conv.parse(body.get_string_from_utf8()) 158 | var json = test_json_conv.get_data() 159 | auth_token = json.token 160 | auth_token_retrieved.emit() 161 | 162 | func on_request_completed(result, response_code, headers, body, pre, post, http_request): 163 | var test_json_conv = JSON.new() 164 | test_json_conv.parse(body.get_string_from_utf8()) 165 | var json = test_json_conv.get_data() 166 | var response = json 167 | if !response.has("choices") : 168 | emit_signal("completion_error", response) 169 | return 170 | var completion = response.choices[0].message 171 | if is_instance_valid(http_request): 172 | http_request.queue_free() 173 | emit_signal("completion_received", completion.content, pre, post) 174 | 175 | func store_config(): 176 | var config = ConfigFile.new() 177 | config.set_value("auth", "machine_id", machine_id) 178 | config.save_encrypted_pass(PREFERENCES_STORAGE_NAME, PREFERENCES_PASS) 179 | 180 | func load_config(): 181 | var config = ConfigFile.new() 182 | var err = config.load_encrypted_pass(PREFERENCES_STORAGE_NAME, PREFERENCES_PASS) 183 | if not session_id: 184 | session_id = gen_hex_str(8) + "-" + gen_hex_str(4) + "-" + gen_hex_str(4) + "-" + gen_hex_str(4) + "-" + gen_hex_str(25) 185 | if err != OK: 186 | machine_id = UUID.v4() 187 | store_config() 188 | return 189 | machine_id = config.get_value("auth", "machine_id", UUID.v4()) 190 | -------------------------------------------------------------------------------- /addons/copilot/CopilotUI.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=7 format=3 uid="uid://rv5dl08lcb8e"] 2 | 3 | [ext_resource type="Script" path="res://addons/copilot/Copilot.gd" id="1_pq1gj"] 4 | [ext_resource type="Script" path="res://addons/copilot/OpenAIChat.gd" id="2"] 5 | [ext_resource type="Material" uid="uid://cccmbprav6vgu" path="res://addons/copilot/small_icon.tres" id="2_gdw4j"] 6 | [ext_resource type="Script" path="res://addons/copilot/OpenAICompletion.gd" id="3_loa2x"] 7 | [ext_resource type="Material" uid="uid://bl1rtf743e4l3" path="res://addons/copilot/large_icon.tres" id="3_xn70b"] 8 | [ext_resource type="Script" path="res://addons/copilot/GithubCopilot.gd" id="6_hmh8w"] 9 | 10 | [node name="Copilot" type="Control"] 11 | layout_mode = 3 12 | anchors_preset = 15 13 | anchor_right = 1.0 14 | anchor_bottom = 1.0 15 | grow_horizontal = 2 16 | grow_vertical = 2 17 | script = ExtResource("1_pq1gj") 18 | icon_shader = ExtResource("2_gdw4j") 19 | highlight_color = Color(0.223529, 0.254902, 0.298039, 1) 20 | 21 | [node name="VBoxParent" type="VBoxContainer" parent="."] 22 | layout_mode = 0 23 | anchor_right = 1.0 24 | anchor_bottom = 1.0 25 | 26 | [node name="Indicator" type="ColorRect" parent="VBoxParent"] 27 | material = ExtResource("3_xn70b") 28 | custom_minimum_size = Vector2(200, 200) 29 | layout_mode = 2 30 | size_flags_horizontal = 4 31 | 32 | [node name="ContextTitle" type="Label" parent="VBoxParent"] 33 | modulate = Color(1, 1, 1, 0.7) 34 | layout_mode = 2 35 | text = "Current Context" 36 | horizontal_alignment = 1 37 | vertical_alignment = 1 38 | autowrap_mode = 3 39 | 40 | [node name="Context" type="Label" parent="VBoxParent"] 41 | layout_mode = 2 42 | horizontal_alignment = 1 43 | vertical_alignment = 1 44 | autowrap_mode = 3 45 | 46 | [node name="Status" type="Label" parent="VBoxParent"] 47 | modulate = Color(1, 1, 1, 0.7) 48 | custom_minimum_size = Vector2(2.08165e-12, 100) 49 | layout_mode = 2 50 | horizontal_alignment = 1 51 | vertical_alignment = 1 52 | autowrap_mode = 3 53 | 54 | [node name="HowToTitle" type="Label" parent="VBoxParent"] 55 | layout_mode = 2 56 | text = "How To Use" 57 | 58 | [node name="Separator1" type="HSeparator" parent="VBoxParent"] 59 | layout_mode = 2 60 | 61 | [node name="HowTo" type="Label" parent="VBoxParent"] 62 | layout_mode = 2 63 | text = "Press the selected shortcut in the code editor to request a completion from Copilot at the current caret position" 64 | autowrap_mode = 3 65 | 66 | [node name="SettingTitle" type="Label" parent="VBoxParent"] 67 | layout_mode = 2 68 | text = "Settings" 69 | 70 | [node name="Separator2" type="HSeparator" parent="VBoxParent"] 71 | layout_mode = 2 72 | 73 | [node name="OpenAiSetting" type="HBoxContainer" parent="VBoxParent"] 74 | custom_minimum_size = Vector2(2.08165e-12, 2.08165e-12) 75 | layout_mode = 2 76 | theme_override_constants/separation = 10 77 | 78 | [node name="Label" type="Label" parent="VBoxParent/OpenAiSetting"] 79 | custom_minimum_size = Vector2(100, 2.08165e-12) 80 | layout_mode = 2 81 | size_flags_horizontal = 3 82 | size_flags_vertical = 1 83 | text = "OpenAI API Key" 84 | vertical_alignment = 1 85 | autowrap_mode = 3 86 | 87 | [node name="VSeparator" type="VSeparator" parent="VBoxParent/OpenAiSetting"] 88 | layout_mode = 2 89 | 90 | [node name="OpenAiKey" type="LineEdit" parent="VBoxParent/OpenAiSetting"] 91 | custom_minimum_size = Vector2(2.08165e-12, 2.08165e-12) 92 | layout_mode = 2 93 | size_flags_horizontal = 10 94 | placeholder_text = "API Key" 95 | secret = true 96 | 97 | [node name="ModelSetting" type="HBoxContainer" parent="VBoxParent"] 98 | custom_minimum_size = Vector2(2.08165e-12, 2.08165e-12) 99 | layout_mode = 2 100 | theme_override_constants/separation = 10 101 | 102 | [node name="Label" type="Label" parent="VBoxParent/ModelSetting"] 103 | custom_minimum_size = Vector2(100, 2.08165e-12) 104 | layout_mode = 2 105 | size_flags_horizontal = 3 106 | size_flags_vertical = 1 107 | text = "Model" 108 | vertical_alignment = 1 109 | 110 | [node name="VSeparator" type="VSeparator" parent="VBoxParent/ModelSetting"] 111 | layout_mode = 2 112 | 113 | [node name="Model" type="OptionButton" parent="VBoxParent/ModelSetting"] 114 | layout_mode = 2 115 | size_flags_horizontal = 10 116 | item_count = 3 117 | selected = 1 118 | fit_to_longest_item = false 119 | popup/item_0/text = "text-davinci-003" 120 | popup/item_0/id = 0 121 | popup/item_1/text = "gpt-3.5-turbo" 122 | popup/item_1/id = 1 123 | popup/item_2/text = "gpt-4" 124 | popup/item_2/id = 2 125 | 126 | [node name="ShortcutSetting" type="HBoxContainer" parent="VBoxParent"] 127 | custom_minimum_size = Vector2(2.08165e-12, 2.08165e-12) 128 | layout_mode = 2 129 | theme_override_constants/separation = 10 130 | 131 | [node name="Label" type="Label" parent="VBoxParent/ShortcutSetting"] 132 | custom_minimum_size = Vector2(100, 2.08165e-12) 133 | layout_mode = 2 134 | size_flags_horizontal = 3 135 | size_flags_vertical = 1 136 | text = "Shortcut" 137 | vertical_alignment = 1 138 | 139 | [node name="VSeparator" type="VSeparator" parent="VBoxParent/ShortcutSetting"] 140 | layout_mode = 2 141 | 142 | [node name="HBoxContainer" type="HBoxContainer" parent="VBoxParent/ShortcutSetting"] 143 | layout_mode = 2 144 | size_flags_horizontal = 10 145 | 146 | [node name="Modifier" type="OptionButton" parent="VBoxParent/ShortcutSetting/HBoxContainer"] 147 | layout_mode = 2 148 | size_flags_horizontal = 10 149 | item_count = 4 150 | selected = 2 151 | popup/item_0/text = "Cmd" 152 | popup/item_0/id = 0 153 | popup/item_1/text = "Option" 154 | popup/item_1/id = 1 155 | popup/item_2/text = "Control" 156 | popup/item_2/id = 2 157 | popup/item_3/text = "Shift" 158 | popup/item_3/id = 3 159 | 160 | [node name="Key" type="OptionButton" parent="VBoxParent/ShortcutSetting/HBoxContainer"] 161 | layout_mode = 2 162 | size_flags_horizontal = 10 163 | item_count = 32 164 | selected = 2 165 | popup/item_0/text = "A" 166 | popup/item_0/id = 0 167 | popup/item_1/text = "B" 168 | popup/item_1/id = 1 169 | popup/item_2/text = "C" 170 | popup/item_2/id = 2 171 | popup/item_3/text = "D" 172 | popup/item_3/id = 3 173 | popup/item_4/text = "E" 174 | popup/item_4/id = 4 175 | popup/item_5/text = "F" 176 | popup/item_5/id = 5 177 | popup/item_6/text = "G" 178 | popup/item_6/id = 6 179 | popup/item_7/text = "H" 180 | popup/item_7/id = 7 181 | popup/item_8/text = "L" 182 | popup/item_8/id = 8 183 | popup/item_9/text = "M" 184 | popup/item_9/id = 9 185 | popup/item_10/text = "N" 186 | popup/item_10/id = 10 187 | popup/item_11/text = "O" 188 | popup/item_11/id = 11 189 | popup/item_12/text = "P" 190 | popup/item_12/id = 12 191 | popup/item_13/text = "Q" 192 | popup/item_13/id = 13 193 | popup/item_14/text = "R" 194 | popup/item_14/id = 14 195 | popup/item_15/text = "S" 196 | popup/item_15/id = 15 197 | popup/item_16/text = "T" 198 | popup/item_16/id = 16 199 | popup/item_17/text = "U" 200 | popup/item_17/id = 17 201 | popup/item_18/text = "V" 202 | popup/item_18/id = 18 203 | popup/item_19/text = "X" 204 | popup/item_19/id = 19 205 | popup/item_20/text = "Y" 206 | popup/item_20/id = 20 207 | popup/item_21/text = "Z" 208 | popup/item_21/id = 21 209 | popup/item_22/text = "1" 210 | popup/item_22/id = 22 211 | popup/item_23/text = "2" 212 | popup/item_23/id = 23 213 | popup/item_24/text = "3" 214 | popup/item_24/id = 24 215 | popup/item_25/text = "4" 216 | popup/item_25/id = 25 217 | popup/item_26/text = "5" 218 | popup/item_26/id = 26 219 | popup/item_27/text = "6" 220 | popup/item_27/id = 27 221 | popup/item_28/text = "7" 222 | popup/item_28/id = 28 223 | popup/item_29/text = "8" 224 | popup/item_29/id = 29 225 | popup/item_30/text = "9" 226 | popup/item_30/id = 30 227 | popup/item_31/text = "0" 228 | popup/item_31/id = 31 229 | 230 | [node name="MultilineSetting" type="HBoxContainer" parent="VBoxParent"] 231 | custom_minimum_size = Vector2(2.08165e-12, 2.08165e-12) 232 | layout_mode = 2 233 | theme_override_constants/separation = 10 234 | 235 | [node name="Label" type="Label" parent="VBoxParent/MultilineSetting"] 236 | custom_minimum_size = Vector2(100, 2.08165e-12) 237 | layout_mode = 2 238 | size_flags_horizontal = 3 239 | size_flags_vertical = 1 240 | text = "Multiline Completions" 241 | vertical_alignment = 1 242 | autowrap_mode = 3 243 | 244 | [node name="VSeparator" type="VSeparator" parent="VBoxParent/MultilineSetting"] 245 | layout_mode = 2 246 | 247 | [node name="Multiline" type="CheckBox" parent="VBoxParent/MultilineSetting"] 248 | layout_mode = 2 249 | size_flags_horizontal = 10 250 | button_pressed = true 251 | text = "Enabled" 252 | 253 | [node name="Info" type="RichTextLabel" parent="VBoxParent"] 254 | layout_mode = 2 255 | focus_mode = 2 256 | fit_content = true 257 | selection_enabled = true 258 | 259 | [node name="LLMs" type="Node" parent="."] 260 | 261 | [node name="OpenAICompletion" type="Node" parent="LLMs"] 262 | script = ExtResource("3_loa2x") 263 | 264 | [node name="OpenAIChat" type="Node" parent="LLMs"] 265 | script = ExtResource("2") 266 | 267 | [node name="GithubCopilot" type="Node" parent="LLMs"] 268 | script = ExtResource("6_hmh8w") 269 | 270 | [node name="Version" type="Label" parent="."] 271 | layout_mode = 1 272 | anchors_preset = 12 273 | anchor_top = 1.0 274 | anchor_right = 1.0 275 | anchor_bottom = 1.0 276 | offset_top = -23.0 277 | grow_horizontal = 2 278 | grow_vertical = 0 279 | horizontal_alignment = 2 280 | vertical_alignment = 2 281 | 282 | [connection signal="text_changed" from="VBoxParent/OpenAiSetting/OpenAiKey" to="." method="_on_open_ai_key_changed"] 283 | [connection signal="item_selected" from="VBoxParent/ModelSetting/Model" to="." method="_on_model_selected"] 284 | [connection signal="item_selected" from="VBoxParent/ShortcutSetting/HBoxContainer/Modifier" to="." method="_on_shortcut_modifier_selected"] 285 | [connection signal="item_selected" from="VBoxParent/ShortcutSetting/HBoxContainer/Key" to="." method="_on_shortcut_key_selected"] 286 | [connection signal="toggled" from="VBoxParent/MultilineSetting/Multiline" to="." method="_on_multiline_toggled"] 287 | [connection signal="meta_clicked" from="VBoxParent/Info" to="." method="on_info_meta_clicked"] 288 | [connection signal="completion_error" from="LLMs/OpenAICompletion" to="." method="_on_code_completion_error"] 289 | [connection signal="completion_received" from="LLMs/OpenAICompletion" to="." method="_on_code_completion_received"] 290 | [connection signal="completion_error" from="LLMs/OpenAIChat" to="." method="_on_code_completion_error"] 291 | [connection signal="completion_received" from="LLMs/OpenAIChat" to="." method="_on_code_completion_received"] 292 | [connection signal="completion_error" from="LLMs/GithubCopilot" to="." method="_on_code_completion_error"] 293 | [connection signal="completion_received" from="LLMs/GithubCopilot" to="." method="_on_code_completion_received"] 294 | -------------------------------------------------------------------------------- /addons/copilot/Copilot.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Control 3 | 4 | @onready var llms = $LLMs 5 | @onready var context_label = $VBoxParent/Context 6 | @onready var status_label = $VBoxParent/Status 7 | @onready var model_select = $VBoxParent/ModelSetting/Model 8 | @onready var shortcut_modifier_select = $VBoxParent/ShortcutSetting/HBoxContainer/Modifier 9 | @onready var shortcut_key_select = $VBoxParent/ShortcutSetting/HBoxContainer/Key 10 | @onready var multiline_toggle = $VBoxParent/MultilineSetting/Multiline 11 | @onready var openai_key_title = $VBoxParent/OpenAiSetting/Label 12 | @onready var openai_key_input = $VBoxParent/OpenAiSetting/OpenAiKey 13 | @onready var version_label = $Version 14 | @onready var info = $VBoxParent/Info 15 | 16 | @export var icon_shader : ShaderMaterial 17 | @export var highlight_color : Color 18 | 19 | var editor_interface : EditorInterface 20 | var screen = "Script" 21 | 22 | var request_code_state = null 23 | var cur_highlight = null 24 | var indicator = null 25 | 26 | var models = {} 27 | var openai_api_key 28 | var cur_model 29 | var cur_shortcut_modifier = "Control" if is_mac() else "Alt" 30 | var cur_shortcut_key = "C" 31 | var allow_multiline = true 32 | 33 | const PREFERENCES_STORAGE_NAME = "user://copilot.cfg" 34 | const PREFERENCES_PASS = "F4fv2Jxpasp20VS5VSp2Yp2v9aNVJ21aRK" 35 | const GITHUB_COPILOT_DISCLAIMER = "Use GitHub Copilot keys at your own risk. Retrieve key by following instructions [url=https://gitlab.com/aaamoon/copilot-gpt4-service?tab=readme-ov-file#obtaining-copilot-token]here[/url]." 36 | 37 | func _ready(): 38 | #Initialize dock, load settings 39 | populate_models() 40 | populate_modifiers() 41 | load_config() 42 | 43 | func populate_models(): 44 | #Add all found models to settings 45 | model_select.clear() 46 | for llm in llms.get_children(): 47 | var new_models = llm._get_models() 48 | for model in new_models: 49 | model_select.add_item(model) 50 | models[model] = get_path_to(llm) 51 | model_select.select(0) 52 | set_model(model_select.get_item_text(0)) 53 | 54 | func populate_modifiers(): 55 | #Add available shortcut modifiers based on platform 56 | shortcut_modifier_select.clear() 57 | var modifiers = ["Alt", "Ctrl", "Shift"] 58 | if is_mac(): modifiers = ["Cmd", "Option", "Control", "Shift"] 59 | for modifier in modifiers: 60 | shortcut_modifier_select.add_item(modifier) 61 | apply_by_value(shortcut_modifier_select, cur_shortcut_modifier) 62 | 63 | func _unhandled_key_input(event): 64 | #Handle input 65 | if event is InputEventKey: 66 | if cur_highlight: 67 | #If completion is shown, TAB will accept it 68 | #and the TAB input ignored 69 | if event.keycode == KEY_TAB: 70 | undo_input() 71 | clear_highlights() 72 | #BACKSPACE will remove it 73 | elif event.keycode == KEY_BACKSPACE: 74 | revert_change() 75 | clear_highlights() 76 | #Any other key press will plainly accept it 77 | else: 78 | clear_highlights() 79 | #If shortcut modifier and key are pressed, request completion 80 | if shortcut_key_pressed(event) and shortcut_modifier_pressed(event): 81 | request_completion() 82 | 83 | func is_mac(): 84 | #Platform check 85 | return OS.get_name() == "macOS" 86 | 87 | func shortcut_key_pressed(event): 88 | #Check if selected shortcut key is pressed 89 | var key_string = OS.get_keycode_string(event.keycode) 90 | return key_string == cur_shortcut_key 91 | 92 | func shortcut_modifier_pressed(event): 93 | #Check if selected shortcut modifier is pressed 94 | match cur_shortcut_modifier: 95 | "Control": 96 | return event.ctrl_pressed 97 | "Ctrl": 98 | return event.ctrl_pressed 99 | "Alt": 100 | return event.alt_pressed 101 | "Option": 102 | return event.alt_pressed 103 | "Shift": 104 | return event.shift_pressed 105 | "Cmd": 106 | return event.meta_pressed 107 | _: 108 | return false 109 | 110 | func clear_highlights(): 111 | #Clear all currently highlighted lines 112 | #and reset request status 113 | request_code_state = null 114 | cur_highlight = null 115 | var editor = get_code_editor() 116 | for line in range(editor.get_line_count()): 117 | editor.set_line_background_color(line, Color(0, 0, 0, 0)) 118 | 119 | func undo_input(): 120 | #Undo last input in code editor 121 | var editor = get_code_editor() 122 | editor.undo() 123 | 124 | func update_loading_indicator(create = false): 125 | #Make sure loading indicator is placed at caret position 126 | if screen != "Script": return 127 | var editor = get_code_editor() 128 | if !editor: return 129 | var line_height = editor.get_line_height() 130 | if !is_instance_valid(indicator): 131 | if !create: return 132 | indicator = ColorRect.new() 133 | indicator.material = icon_shader 134 | indicator.custom_minimum_size = Vector2(line_height, line_height) 135 | editor.add_child(indicator) 136 | var pos = editor.get_caret_draw_pos() 137 | var pre_post = get_pre_post() 138 | #Caret position returned from Godot is not reliable 139 | #Needs to be adjusted for empty lines 140 | var is_on_empty_line = pre_post[0].right(1) == "\n" 141 | var offset = line_height/2-1 if is_on_empty_line else line_height-1 142 | indicator.position = Vector2(pos.x, pos.y - offset) 143 | editor.editable = false 144 | 145 | func remove_loading_indicator(): 146 | #Free loading indicator, and return editor to editable state 147 | if is_instance_valid(indicator): indicator.queue_free() 148 | set_status("") 149 | var editor = get_code_editor() 150 | editor.editable = true 151 | 152 | func set_status(text): 153 | #Update status label in dock 154 | status_label.text = "" 155 | 156 | func insert_completion(content: String, pre, post): 157 | #Overwrite code editor text to insert received completion 158 | var editor = get_code_editor() 159 | var scroll = editor.scroll_vertical 160 | 161 | var caret_text = pre + content 162 | var lines_from = pre.split("\n") 163 | var lines_to = caret_text.split("\n") 164 | 165 | cur_highlight = [lines_from.size(), lines_to.size()] 166 | 167 | editor.set_text(pre + content + post) 168 | editor.set_caret_line(lines_to.size()) 169 | editor.set_caret_column(lines_to[-1].length()) 170 | editor.scroll_vertical = scroll 171 | editor.update_code_completion_options(false) 172 | 173 | func revert_change(): 174 | #Revert inserted completion 175 | var code_edit = get_code_editor() 176 | var scroll = code_edit.scroll_vertical 177 | var old_text = request_code_state[0] + request_code_state[1] 178 | var lines_from = request_code_state[0].strip_edges(false, true).split("\n") 179 | code_edit.set_text(old_text) 180 | code_edit.set_caret_line(lines_from.size()-1) 181 | code_edit.set_caret_column(lines_from[-1].length()) 182 | code_edit.scroll_vertical = scroll 183 | clear_highlights() 184 | 185 | func _process(delta): 186 | #Update visuals and context label 187 | update_highlights() 188 | update_loading_indicator() 189 | update_context() 190 | 191 | func update_highlights(): 192 | #Make sure highlighted lines persist until explicitely removed 193 | #via key input 194 | if cur_highlight: 195 | var editor = get_code_editor() 196 | for line in range(cur_highlight[0]-1, cur_highlight[1]): 197 | editor.set_line_background_color(line, highlight_color) 198 | 199 | func update_context(): 200 | #Show currently edited file in dock 201 | var script = get_current_script() 202 | if script: context_label.text = script.resource_path.get_file() 203 | 204 | func on_main_screen_changed(_screen): 205 | #Track current editor screen (2D, 3D, Script) 206 | screen = _screen 207 | 208 | func get_current_script(): 209 | #Get currently edited script 210 | if !editor_interface: return 211 | var script_editor = editor_interface.get_script_editor() 212 | return script_editor.get_current_script() 213 | 214 | func get_code_editor(): 215 | #Get currently used code editor 216 | #This does not return the shader editor! 217 | if !editor_interface: return 218 | var script_editor = editor_interface.get_script_editor() 219 | var base_editor = script_editor.get_current_editor() 220 | if base_editor: 221 | var code_edit = base_editor.get_base_editor() 222 | return code_edit 223 | return null 224 | 225 | func request_completion(): 226 | #Get current code and request completion from active model 227 | if request_code_state: return 228 | set_status("Asking %s..." % cur_model) 229 | update_loading_indicator(true) 230 | var pre_post = get_pre_post() 231 | var llm = get_llm() 232 | if !llm: return 233 | llm._send_user_prompt(pre_post[0], pre_post[1]) 234 | request_code_state = pre_post 235 | 236 | func get_pre_post(): 237 | #Split current code based on caret position 238 | var editor = get_code_editor() 239 | var text = editor.get_text() 240 | var pos = Vector2(editor.get_caret_line(), editor.get_caret_column()) 241 | var pre = "" 242 | var post = "" 243 | for i in range(pos.x): 244 | pre += editor.get_line(i) + "\n" 245 | pre += editor.get_line(pos.x).substr(0,pos.y) 246 | post += editor.get_line(pos.x).substr(pos.y) + "\n" 247 | for ii in range(pos.x+1, editor.get_line_count()): 248 | post += editor.get_line(ii) + "\n" 249 | return [pre, post] 250 | 251 | func get_llm(): 252 | #Get currently active llm and set active model 253 | var llm = get_node(models[cur_model]) 254 | llm._set_api_key(openai_api_key) 255 | llm._set_model(cur_model) 256 | llm._set_multiline(allow_multiline) 257 | return llm 258 | 259 | func matches_request_state(pre, post): 260 | #Check if code passed for completion request matches current code 261 | return request_code_state[0] == pre and request_code_state[1] == post 262 | 263 | func set_openai_api_key(key): 264 | #Apply API key 265 | openai_api_key = key 266 | 267 | func set_model(model_name): 268 | #Apply selected model 269 | cur_model = model_name 270 | # Handle some special model scenarios 271 | if "github-copilot" in model_name: 272 | openai_key_title.text = "GitHub Copilot API Key" 273 | info.parse_bbcode(GITHUB_COPILOT_DISCLAIMER) 274 | info.show() 275 | else: 276 | openai_key_title.text = "OpenAI API Key" 277 | info.hide() 278 | 279 | func set_shortcut_modifier(modifier): 280 | #Apply selected shortcut modifier 281 | cur_shortcut_modifier = modifier 282 | 283 | func set_shortcut_key(key): 284 | #Apply selected shortcut key 285 | cur_shortcut_key = key 286 | 287 | func set_multiline(active): 288 | #Apply selected multiline setting 289 | allow_multiline = active 290 | 291 | func _on_code_completion_received(completion, pre, post): 292 | #Attempt to insert received code completion 293 | remove_loading_indicator() 294 | if matches_request_state(pre, post): 295 | insert_completion(completion, pre, post) 296 | else: 297 | clear_highlights() 298 | 299 | func _on_code_completion_error(error): 300 | #Display error 301 | remove_loading_indicator() 302 | clear_highlights() 303 | push_error(error) 304 | 305 | func _on_open_ai_key_changed(key): 306 | #Apply setting and store in config file 307 | set_openai_api_key(key) 308 | store_config() 309 | 310 | func _on_model_selected(index): 311 | #Apply setting and store in config file 312 | set_model(model_select.get_item_text(index)) 313 | store_config() 314 | 315 | func _on_shortcut_modifier_selected(index): 316 | #Apply setting and store in config file 317 | set_shortcut_modifier(shortcut_modifier_select.get_item_text(index)) 318 | store_config() 319 | 320 | func _on_shortcut_key_selected(index): 321 | #Apply setting and store in config file 322 | set_shortcut_key(shortcut_key_select.get_item_text(index)) 323 | store_config() 324 | 325 | func _on_multiline_toggled(button_pressed): 326 | #Apply setting and store in config file 327 | set_multiline(button_pressed) 328 | store_config() 329 | 330 | func store_config(): 331 | #Store current setting in config file 332 | var config = ConfigFile.new() 333 | config.set_value("preferences", "model", cur_model) 334 | config.set_value("preferences", "shortcut_modifier", cur_shortcut_modifier) 335 | config.set_value("preferences", "shortcut_key", cur_shortcut_key) 336 | config.set_value("preferences", "allow_multiline", allow_multiline) 337 | config.set_value("keys", "openai", openai_api_key) 338 | config.save_encrypted_pass(PREFERENCES_STORAGE_NAME, PREFERENCES_PASS) 339 | 340 | func load_config(): 341 | #Retrieve current settings from config file 342 | var config = ConfigFile.new() 343 | var err = config.load_encrypted_pass(PREFERENCES_STORAGE_NAME, PREFERENCES_PASS) 344 | if err != OK: return 345 | cur_model = config.get_value("preferences", "model", cur_model) 346 | apply_by_value(model_select, cur_model) 347 | set_model(model_select.get_item_text(model_select.selected)) 348 | cur_shortcut_modifier = config.get_value("preferences", "shortcut_modifier", cur_shortcut_modifier) 349 | apply_by_value(shortcut_modifier_select, cur_shortcut_modifier) 350 | cur_shortcut_key = config.get_value("preferences", "shortcut_key", cur_shortcut_key) 351 | apply_by_value(shortcut_key_select, cur_shortcut_key) 352 | allow_multiline = config.get_value("preferences", "allow_multiline", allow_multiline) 353 | multiline_toggle.set_pressed_no_signal(allow_multiline) 354 | openai_api_key = config.get_value("keys", "openai", "") 355 | openai_key_input.text = openai_api_key 356 | 357 | func apply_by_value(option_button, value): 358 | #Select item for option button based on value instead of index 359 | for i in option_button.item_count: 360 | if option_button.get_item_text(i) == value: 361 | option_button.select(i) 362 | 363 | func set_version(version): 364 | version_label.text = "v%s" % version 365 | 366 | 367 | func on_info_meta_clicked(meta): 368 | OS.shell_open(meta) 369 | --------------------------------------------------------------------------------