Created LanguageTool Plugin
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
@tool
|
||||
extends Node
|
||||
|
||||
class_name LanguageToolCorrectionOverlay
|
||||
|
||||
const CORRECTION_OVERLAY = preload("res://addons/languagetool/scenes/correction_overlay.tscn")
|
||||
var instantiatedOverlay: LanguageToolCorrectionOverlayReferences
|
||||
|
||||
func _enter_tree() -> void:
|
||||
instantiatedOverlay = CORRECTION_OVERLAY.instantiate()
|
||||
add_child(instantiatedOverlay)
|
||||
hideOverlay()
|
||||
|
||||
func _ready():
|
||||
instantiatedOverlay.close_button.pressed.connect(hideOverlay)
|
||||
|
||||
func _exit_tree() -> void:
|
||||
pass
|
||||
|
||||
func hideOverlay():
|
||||
instantiatedOverlay.hide()
|
||||
|
||||
func showOverlay(position:Vector2, width: float, _match:LanguageToolApiWrapper.LanguageToolCheckResponse.Match, replacement_clicked : Callable):
|
||||
#print(instantiatedOverlay.test)
|
||||
instantiatedOverlay.show()
|
||||
instantiatedOverlay.global_position = position
|
||||
instantiatedOverlay.size = Vector2(0,0)
|
||||
instantiatedOverlay.custom_minimum_size = Vector2(width, 0)
|
||||
instantiatedOverlay.category_label.text = _match.rule.category.name
|
||||
instantiatedOverlay.description_label.text = _match.message
|
||||
|
||||
for c in instantiatedOverlay.replacements.get_children():
|
||||
c.free()
|
||||
|
||||
for r in _match.replacements:
|
||||
var replacementButton = Button.new()
|
||||
replacementButton.text = r
|
||||
replacementButton.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
replacementButton.pressed.connect(func():replacement_clicked.call(r))
|
||||
instantiatedOverlay.replacements.add_child(replacementButton)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://ct2t8rr000prq
|
||||
@@ -0,0 +1,9 @@
|
||||
@tool
|
||||
extends VBoxContainer
|
||||
|
||||
class_name LanguageToolCorrectionOverlayReferences
|
||||
|
||||
@onready var category_label: Label = $MarginContainer/PanelContainer/MarginContainer/VBoxContainer/HBoxContainer/CategoryLabel
|
||||
@onready var description_label: Label = $MarginContainer/PanelContainer/MarginContainer/VBoxContainer/DescriptionLabel
|
||||
@onready var replacements: HFlowContainer = $MarginContainer/PanelContainer/MarginContainer/VBoxContainer/Replacements
|
||||
@onready var close_button: Button = $MarginContainer/PanelContainer/MarginContainer/VBoxContainer/HBoxContainer/CloseButton
|
||||
@@ -0,0 +1 @@
|
||||
uid://dkyiuvuc2w2xc
|
||||
@@ -0,0 +1,32 @@
|
||||
extends SyntaxHighlighter
|
||||
|
||||
class_name LanguageToolErrorSyntaxHighlighter
|
||||
|
||||
var check:LanguageToolApiWrapper.LanguageToolCheckResponse
|
||||
|
||||
func _init(check:LanguageToolApiWrapper.LanguageToolCheckResponse):
|
||||
self.check = check
|
||||
|
||||
func _get_line_syntax_highlighting(line: int) -> Dictionary:
|
||||
var normalColor = EditorInterface.get_base_control().get_theme_color("font_color", "Editor")
|
||||
var errorColor = EditorInterface.get_base_control().get_theme_color("error_color", "Editor")
|
||||
var warningColor = EditorInterface.get_base_control().get_theme_color("warning_color", "Editor")
|
||||
var successColor = EditorInterface.get_base_control().get_theme_color("success_color", "Editor")
|
||||
|
||||
var retval = {}
|
||||
|
||||
for m:LanguageToolApiWrapper.LanguageToolCheckResponse.Match in check.matches:
|
||||
var row_column = LanguageToolUtils.offset_to_row_column(m.offset,get_text_edit().text)
|
||||
if row_column[0] != line:
|
||||
continue
|
||||
match m.rule.category.id:
|
||||
"GRAMMAR":
|
||||
retval[row_column[1]] = {"color":warningColor}
|
||||
"TYPOS":
|
||||
retval[row_column[1]] = {"color":errorColor}
|
||||
_:
|
||||
retval[row_column[1]] = {"color":successColor}
|
||||
|
||||
retval[row_column[1]+m.length] = {"color":normalColor}
|
||||
|
||||
return retval
|
||||
@@ -0,0 +1 @@
|
||||
uid://csxcr0bsetagc
|
||||
@@ -0,0 +1,112 @@
|
||||
extends Node
|
||||
|
||||
class_name LanguagetToolPlugin
|
||||
|
||||
var api: LanguageToolApiWrapper
|
||||
var overlay: LanguageToolCorrectionOverlay
|
||||
|
||||
var checkDict: Dictionary[String,LanguageToolApiWrapper.LanguageToolCheckResponse] = {}
|
||||
|
||||
func _enter_tree() -> void:
|
||||
api = LanguageToolApiWrapper.new()
|
||||
add_child(api)
|
||||
overlay = LanguageToolCorrectionOverlay.new()
|
||||
add_child(overlay)
|
||||
|
||||
func _exit_tree() -> void:
|
||||
pass
|
||||
|
||||
func check_new_inspector():
|
||||
overlay.hideOverlay()
|
||||
|
||||
var textEdits: Array[TextEdit] = _find_multiline_text_edits()
|
||||
for te: TextEdit in textEdits:
|
||||
te.text_changed.connect(func():_on_text_changed(te))
|
||||
te.focus_exited.connect(func():_on_focus_lost(te))
|
||||
te.caret_changed.connect(func():_on_caret_changed(te))
|
||||
|
||||
_check_text(te)
|
||||
_mark_errors_in_text(te)
|
||||
|
||||
|
||||
func _on_text_changed(textEdit: TextEdit):
|
||||
_mark_errors_in_text(textEdit)
|
||||
overlay.hideOverlay()
|
||||
|
||||
func _on_focus_lost(textEdit: TextEdit):
|
||||
_check_text(textEdit)
|
||||
_mark_errors_in_text(textEdit)
|
||||
|
||||
func _on_caret_changed(textEdit: TextEdit):
|
||||
if(!checkDict.has(textEdit.text)):
|
||||
return
|
||||
var check: LanguageToolApiWrapper.LanguageToolCheckResponse = checkDict[textEdit.text]
|
||||
|
||||
# find match at caret
|
||||
var caret_offset:int = LanguageToolUtils.row_column_to_offset(textEdit.get_caret_line(), textEdit.get_caret_column(),textEdit.text)
|
||||
var _match:LanguageToolApiWrapper.LanguageToolCheckResponse.Match = null
|
||||
for m in check.matches:
|
||||
if m.offset <= caret_offset and m.offset + m.length >= caret_offset:
|
||||
_match = m
|
||||
break
|
||||
|
||||
if _match != null:
|
||||
var edit_global_rect = textEdit.get_global_rect()
|
||||
overlay.showOverlay(
|
||||
edit_global_rect.position + Vector2(0,edit_global_rect.size.y),
|
||||
edit_global_rect.size.x,
|
||||
_match,
|
||||
func(newText):_apply_text_change(textEdit,newText,_match))
|
||||
else:
|
||||
overlay.hideOverlay()
|
||||
pass
|
||||
|
||||
func _check_text(textEdit: TextEdit):
|
||||
if textEdit.text == "":
|
||||
return
|
||||
|
||||
if checkDict.has(textEdit.text):
|
||||
return
|
||||
|
||||
var response = api.check(textEdit.text)
|
||||
checkDict[textEdit.text] = response
|
||||
|
||||
|
||||
func _mark_errors_in_text(textEdit: TextEdit):
|
||||
if(!checkDict.has(textEdit.text)):
|
||||
textEdit.syntax_highlighter=null
|
||||
return
|
||||
var check: LanguageToolApiWrapper.LanguageToolCheckResponse = checkDict[textEdit.text]
|
||||
textEdit.syntax_highlighter = LanguageToolErrorSyntaxHighlighter.new(check)
|
||||
|
||||
func _apply_text_change(textEdit:TextEdit, newText: String, _match:LanguageToolApiWrapper.LanguageToolCheckResponse.Match):
|
||||
var oldText = textEdit.text
|
||||
var removedOldWord = oldText.erase(_match.offset,_match.length)
|
||||
var newWordInserted = removedOldWord.insert(_match.offset,newText)
|
||||
textEdit.text = newWordInserted
|
||||
textEdit.text_changed.emit()
|
||||
overlay.hideOverlay()
|
||||
_check_text(textEdit)
|
||||
_mark_errors_in_text(textEdit)
|
||||
|
||||
func _find_multiline_text_edits()->Array[TextEdit]:
|
||||
var multilinteTexts:Array[Node] = _find_recursive(
|
||||
EditorInterface.get_inspector().get_child(0).get_child(2),
|
||||
"EditorPropertyMultilineText");
|
||||
|
||||
var textEditors:Array[TextEdit]
|
||||
textEditors.assign( multilinteTexts.map(func(c):return c.get_child(0).get_child(0) as TextEdit))
|
||||
|
||||
return textEditors
|
||||
|
||||
func _find_recursive(node: Node, type: Variant) -> Array[Node]:
|
||||
if type is String:
|
||||
if node.get_class() == type:
|
||||
return [node]
|
||||
elif is_instance_of(node, type):
|
||||
return [node]
|
||||
|
||||
var retval: Array[Node] = []
|
||||
for child in node.get_children():
|
||||
retval.append_array(_find_recursive(child, type))
|
||||
return retval
|
||||
@@ -0,0 +1 @@
|
||||
uid://bi8yv26eglkso
|
||||
@@ -0,0 +1,32 @@
|
||||
@tool
|
||||
extends Object
|
||||
|
||||
class_name LanguageToolUtils
|
||||
|
||||
static func offset_to_row_column(offset:int, text:String)->Vector2i:
|
||||
var row:int = 0
|
||||
var column:int = 0
|
||||
|
||||
if offset > text.length():
|
||||
return Vector2i(-1,-1)
|
||||
|
||||
for i in offset:
|
||||
if text[i] == "\n":
|
||||
row+=1
|
||||
column = 0
|
||||
else:
|
||||
column+=1
|
||||
return Vector2i(row, column)
|
||||
|
||||
static func row_column_to_offset(row:int, column:int, text:String) -> int:
|
||||
var current_row:int = 0
|
||||
var current_column:int = 0
|
||||
for i in text.length():
|
||||
if current_row == row and current_column == column:
|
||||
return i
|
||||
if text[i] == "\n":
|
||||
current_row += 1
|
||||
current_column = 0
|
||||
else:
|
||||
current_column += 1
|
||||
return -1
|
||||
@@ -0,0 +1 @@
|
||||
uid://q01v4f8pfgfe
|
||||
@@ -0,0 +1,211 @@
|
||||
@tool
|
||||
class_name LanguageToolApiWrapper
|
||||
extends Node
|
||||
|
||||
const BASE_URL := "https://api.languagetoolplus.com/v2"
|
||||
|
||||
func _make_request(endpoint: String, method: HTTPClient.Method = HTTPClient.METHOD_GET, data: Dictionary = {}, headers: Dictionary = {}):
|
||||
var url = BASE_URL + endpoint
|
||||
var scheme_split = url.split("://")
|
||||
var scheme = scheme_split[0]
|
||||
var rest = scheme_split[1]
|
||||
var host_and_path = rest.split("/", false, 1)
|
||||
var host = host_and_path[0]
|
||||
var path = "/" + host_and_path[1] if host_and_path.size() > 1 else "/"
|
||||
var port = 443 if scheme == "https" else 80
|
||||
|
||||
var client = HTTPClient.new()
|
||||
var tlsOptions: TLSOptions = (TLSOptions.client() if scheme == "https" else null)
|
||||
var err = client.connect_to_host(host, port, tlsOptions)
|
||||
if err != OK:
|
||||
push_error("Failed to connect to host: " + str(err))
|
||||
return null
|
||||
|
||||
while client.get_status() in [HTTPClient.STATUS_CONNECTING, HTTPClient.STATUS_RESOLVING]:
|
||||
client.poll()
|
||||
OS.delay_msec(10)
|
||||
|
||||
var header_array = []
|
||||
for k in headers.keys():
|
||||
header_array.append(str(k) + ": " + str(headers[k]))
|
||||
|
||||
var body = ""
|
||||
if method == HTTPClient.METHOD_POST:
|
||||
body = ""
|
||||
if data.size() > 0:
|
||||
body = client.query_string_from_dict(data)
|
||||
header_array.append("Content-Type: application/x-www-form-urlencoded")
|
||||
header_array.append("Content-Length: " + str(body.length()))
|
||||
client.request(HTTPClient.METHOD_POST, path, header_array, body)
|
||||
else:
|
||||
if data.size() > 0:
|
||||
path += "?" + client.query_string_from_dict(data)
|
||||
client.request(HTTPClient.METHOD_GET, path, header_array)
|
||||
|
||||
while client.get_status() == HTTPClient.STATUS_REQUESTING:
|
||||
client.poll()
|
||||
OS.delay_msec(10)
|
||||
|
||||
var response = ""
|
||||
while client.get_status() == HTTPClient.STATUS_BODY or client.has_response():
|
||||
client.poll()
|
||||
var chunk = client.read_response_body_chunk()
|
||||
if chunk.size() == 0:
|
||||
break
|
||||
response += chunk.get_string_from_utf8()
|
||||
OS.delay_msec(10)
|
||||
|
||||
var resp_code = client.get_response_code()
|
||||
if resp_code != 200:
|
||||
push_error("HTTP error: " + str(resp_code) + "\\n" + response)
|
||||
return null
|
||||
|
||||
var json = JSON.new()
|
||||
var json_err = json.parse(response)
|
||||
if json_err != OK:
|
||||
push_error("JSON parse error: " + str(json_err) + "\\n" + response)
|
||||
return null
|
||||
return json.get_data()
|
||||
|
||||
func check(text: String, language: String = "auto", opts: Dictionary = {}) -> LanguageToolCheckResponse:
|
||||
var data = {
|
||||
"text": text,
|
||||
"language": language
|
||||
}
|
||||
for k in opts.keys():
|
||||
data[k] = opts[k]
|
||||
print("Checking text: "+text)
|
||||
return LanguageToolCheckResponse.new(_make_request("/check", HTTPClient.METHOD_POST, data))
|
||||
|
||||
func get_languages():
|
||||
return _make_request("/languages", HTTPClient.METHOD_GET)
|
||||
|
||||
func list_words(username: String, apiKey: String, offset: int = 0, limit: int = 10, dicts: String = ""):
|
||||
var data = {
|
||||
"username": username,
|
||||
"apiKey": apiKey,
|
||||
"offset": offset,
|
||||
"limit": limit
|
||||
}
|
||||
if dicts != "":
|
||||
data["dicts"] = dicts
|
||||
return _make_request("/words", HTTPClient.METHOD_GET, data)
|
||||
|
||||
func add_word(word: String, username: String, apiKey: String, dict: String = ""):
|
||||
var data = {
|
||||
"word": word,
|
||||
"username": username,
|
||||
"apiKey": apiKey
|
||||
}
|
||||
if dict != "":
|
||||
data["dict"] = dict
|
||||
return _make_request("/words/add", HTTPClient.METHOD_POST, data)
|
||||
|
||||
func delete_word(word: String, username: String, apiKey: String, dict: String = ""):
|
||||
var data = {
|
||||
"word": word,
|
||||
"username": username,
|
||||
"apiKey": apiKey
|
||||
}
|
||||
if dict != "":
|
||||
data["dict"] = dict
|
||||
return _make_request("/words/delete", HTTPClient.METHOD_POST, data)
|
||||
|
||||
static func percent_encode(text: String) -> String:
|
||||
return text.uri_encode()
|
||||
|
||||
class LanguageToolCheckResponse:
|
||||
|
||||
# Software info
|
||||
var software_name: String
|
||||
var software_version: String
|
||||
var software_build_date: String
|
||||
var software_api_version: int
|
||||
var software_status: String = ""
|
||||
var software_premium: bool = false
|
||||
|
||||
# Language info
|
||||
var language_name: String
|
||||
var language_code: String
|
||||
var detected_language_name: String
|
||||
var detected_language_code: String
|
||||
|
||||
# Match structure
|
||||
class Match:
|
||||
var message: String
|
||||
var short_message: String = ""
|
||||
var offset: int
|
||||
var length: int
|
||||
var replacements: Array[String] = []
|
||||
var context_text: String
|
||||
var context_offset: int
|
||||
var context_length: int
|
||||
var sentence: String
|
||||
class Rule:
|
||||
var id: String
|
||||
var sub_id: String = ""
|
||||
var description: String
|
||||
var urls: Array[String] = []
|
||||
var issue_type: String = ""
|
||||
class Category:
|
||||
var id: String
|
||||
var name: String
|
||||
var category: Category
|
||||
var rule: Rule
|
||||
|
||||
var matches: Array[Match] = []
|
||||
|
||||
func _init(response: Variant) -> void:
|
||||
# Parse software
|
||||
var sw = response.software if response.has("software") else {}
|
||||
software_name = sw.name if sw.has("name") else ""
|
||||
software_version = sw.version if sw.has("version") else ""
|
||||
software_build_date = sw.buildDate if sw.has("buildDate") else ""
|
||||
software_api_version = sw.apiVersion if sw.has("apiVersion") else 0
|
||||
software_status = sw.status if sw.has("status") else ""
|
||||
software_premium = sw.premium if sw.has("premium") else false
|
||||
|
||||
# Parse language
|
||||
var lang = response.language if response.has("language") else {}
|
||||
language_name = lang.name if lang.has("name") else ""
|
||||
language_code = lang.code if lang.has("code") else ""
|
||||
var det_lang = lang.detectedLanguage if lang.has("detectedLanguage") else {}
|
||||
detected_language_name = det_lang.name if det_lang.has("name") else ""
|
||||
detected_language_code = det_lang.code if det_lang.has("code") else ""
|
||||
|
||||
# Parse matches
|
||||
matches = []
|
||||
var matches_arr = response.matches if response.has("matches") else []
|
||||
for m in matches_arr:
|
||||
var _match = Match.new()
|
||||
_match.message = m.message if m.has("message") else ""
|
||||
_match.short_message = m.shortMessage if m.has("shortMessage") else ""
|
||||
_match.offset = m.offset if m.has("offset") else 0
|
||||
_match.length = m.length if m.has("length") else 0
|
||||
#_match.replacements = []
|
||||
var replacements_arr = m.replacements if m.has("replacements") else []
|
||||
for r in replacements_arr:
|
||||
_match.replacements.append(r.value if r.has("value") else "")
|
||||
var ctx = m.context if m.has("context") else {}
|
||||
_match.context_text = ctx.text if ctx.has("text") else ""
|
||||
_match.context_offset = ctx.offset if ctx.has("offset") else 0
|
||||
_match.context_length = ctx.length if ctx.has("length") else 0
|
||||
_match.sentence = m.sentence if m.has("sentence") else ""
|
||||
var rule_dict = m.rule if m.has("rule") else {}
|
||||
var rule = Match.Rule.new()
|
||||
rule.id = rule_dict.id if rule_dict.has("id") else ""
|
||||
rule.sub_id = rule_dict.subId if rule_dict.has("subId") else ""
|
||||
rule.description = rule_dict.description if rule_dict.has("description") else ""
|
||||
#rule.urls = []
|
||||
var urls_arr = rule_dict.urls if rule_dict.has("urls") else []
|
||||
for u in urls_arr:
|
||||
rule.urls.append(u.value if u.has("value") else "")
|
||||
rule.issue_type = rule_dict.issueType if rule_dict.has("issueType") else ""
|
||||
var cat = rule_dict.category if rule_dict.has("category") else {}
|
||||
var category = Match.Rule.Category.new()
|
||||
category.id = cat.id if cat.has("id") else ""
|
||||
category.name = cat.name if cat.has("name") else ""
|
||||
rule.category = category
|
||||
_match.rule = rule
|
||||
matches.append(_match)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://bkyd022t8ugkw
|
||||
Reference in New Issue
Block a user