update: phantoms rather than regions

This commit is contained in:
TheSecEng 2020-04-15 13:28:44 -04:00
parent 2edda28695
commit 7ee91d7feb
No known key found for this signature in database
GPG Key ID: A7C3BA459E8C5C4E
13 changed files with 124 additions and 95 deletions

View File

@ -3,21 +3,21 @@ import os
import re import re
import subprocess import subprocess
import sys import sys
from xml.dom import minidom
from xml.etree import ElementTree from xml.etree import ElementTree
import sublime import sublime
import sublime_plugin import sublime_plugin
from . import simplejson as json from .libs import simplejson as json
from .simplejson import OrderedDict from .libs.simplejson import OrderedDict
SUBLIME_MAJOR_VERSION = int(sublime.version()) / 1000 SUBLIME_MAJOR_VERSION = int(sublime.version()) / 1000
s = sublime.load_settings("Pretty JSON.sublime-settings")
xml_syntax = "Packages/XML/XML.tmLanguage" xml_syntax = "Packages/XML/XML.tmLanguage"
json_syntax = "Packages/JSON/JSON.tmLanguage" json_syntax = "Packages/JSON/JSON.tmLanguage"
jq_exists = False jq_exists = False
jq_init = False jq_init = False
@ -51,10 +51,9 @@ def check_jq():
jq_exists = False jq_exists = False
s = sublime.load_settings("Pretty JSON.sublime-settings")
class PrettyJsonBaseCommand: class PrettyJsonBaseCommand:
json_error_matcher = re.compile(r"line (\d+)") json_error_matcher = re.compile(r"line (\d+)")
json_char_matcher = re.compile(r"char (\d+)")
force_sorting = False force_sorting = False
@staticmethod @staticmethod
@ -104,7 +103,7 @@ class PrettyJsonBaseCommand:
def json_dumps_minified(obj): def json_dumps_minified(obj):
line_separator = s.get("line_separator", ",") line_separator = s.get("line_separator", ",")
""":type : str""" """:type : str"""
value_separator = s.get("value_separator", ": ") value_separator = s.get("value_separator", ":")
""":type : str""" """:type : str"""
sort_keys = s.get("sort_keys", False) sort_keys = s.get("sort_keys", False)
@ -121,12 +120,11 @@ class PrettyJsonBaseCommand:
def reindent(self, text, selection): def reindent(self, text, selection):
current_line = self.view.line(selection.begin()) current_line = self.view.line(selection.begin())
text_before_sel = sublime.Region( text_before_sel = sublime.Region(current_line.begin(), selection.begin())
current_line.begin(), selection.begin())
reindent_mode = s.get('reindent_block', 'minimal') reindent_mode = s.get("reindent_block", "minimal")
if reindent_mode == 'start': if reindent_mode == "start":
# Reindent to the column where the selection starts # Reindent to the column where the selection starts
space_number = text_before_sel.size() space_number = text_before_sel.size()
indent_space = " " * space_number indent_space = " " * space_number
@ -136,28 +134,39 @@ class PrettyJsonBaseCommand:
# Extracts the spaces at the start of the line to use them # Extracts the spaces at the start of the line to use them
# as padding # as padding
indent_space = re.search( indent_space = re.search("^\s*", self.view.substr(text_before_sel)).group(0)
'^\s*', self.view.substr(text_before_sel)).group(0)
lines = text.split('\n') lines = text.split("\n")
# Pad every line except the first one # Pad every line except the first one
i = 1 i = 1
while (i < len(lines)): while i < len(lines):
lines[i] = indent_space + lines[i] lines[i] = indent_space + lines[i]
i += 1 i += 1
return "\n".join(lines) return "\n".join(lines)
def show_exception(self, region, msg):
sublime.status_message("[Error]: {}".format(msg))
if region is None:
return
self.highlight_error(region=region, message="{}".format(msg))
def highlight_error(self, message): def highlight_error(self, region, message):
self.phantom_set = sublime.PhantomSet(self.view, "json_errors")
self.phantoms = list()
self.view.erase_regions("json_errors") matches = self.json_error_matcher.search(message)
self.view.erase_status("json_errors") char_match = self.json_char_matcher.search(message)
if char_match:
m = self.json_error_matcher.search(message) if region.a > region.b:
if m: region.b += int(char_match.group(1)) + 1
line = int(m.group(1)) - 1 region.a = region.b + 1
else:
region.a += int(char_match.group(1)) + 1
region.b = region.a + 1
if matches:
line = int(matches.group(1)) - 1
# sometime we need to highlight one line above # sometime we need to highlight one line above
if "','" in message and "delimiter" in message: if "','" in message and "delimiter" in message:
@ -165,7 +174,8 @@ class PrettyJsonBaseCommand:
self.view.full_line(self.view.text_point(line - 1, 0)) self.view.full_line(self.view.text_point(line - 1, 0))
) )
if ( if (
line_content.strip()[-1] != "," line_content.strip()
and line_content.strip()[-1] != ","
and line_content.strip() != "{" and line_content.strip() != "{"
and line_content.strip() != "}" and line_content.strip() != "}"
): ):
@ -179,19 +189,38 @@ class PrettyJsonBaseCommand:
if len(quotes) % 2 != 0 and len(quotes) != 0: if len(quotes) % 2 != 0 and len(quotes) != 0:
line -= 1 line -= 1
regions = [ self.phantoms.append(
self.view.full_line(self.view.text_point(line, 0)), sublime.Phantom(
] region,
self.create_phantom_html(message, "error"),
self.view.add_regions( sublime.LAYOUT_BELOW,
"json_errors", regions, "invalid", "dot", sublime.DRAW_OUTLINED self.navigation,
) )
self.view.show(regions[0]) )
self.phantom_set.update(self.phantoms)
self.view.set_status("json_errors", message) self.view.set_status("json_errors", message)
def show_exception(self, msg): # Description: Taken from https://github.com/sublimelsp/LSP/blob/master/plugin/diagnostics.py
sublime.status_message("[Error]: {}".format(msg)) # - Thanks to the LSP Team
self.highlight_error("{}".format(msg)) def create_phantom_html(self, content: str, severity: str) -> str:
stylesheet = sublime.load_resource("Packages/SublimePrettyJson/phantom.css")
return """<body id=inline-error>
<style>{}</style>
<div class="{}"></div>
<div class="{} container">
<div class="toolbar">
<a href="hide">×</a>
</div>
<div class="content">{}</div>
</div>
</body>""".format(
stylesheet, severity, severity, content
)
def navigation(self, href):
if href == "hide":
self.phantoms = list()
self.phantom_set.update(self.phantoms)
def syntax_to_json(self): def syntax_to_json(self):
""" Changes syntax to JSON if its in plain text """ """ Changes syntax to JSON if its in plain text """
@ -208,6 +237,7 @@ class PrettyJsonValidate(PrettyJsonBaseCommand, sublime_plugin.TextCommand):
continue continue
elif region.empty() and s.get("use_entire_file_if_no_selection", True): elif region.empty() and s.get("use_entire_file_if_no_selection", True):
selection = sublime.Region(0, self.view.size()) selection = sublime.Region(0, self.view.size())
region = sublime.Region(0, self.view.size())
else: else:
selection = region selection = region
@ -215,12 +245,13 @@ class PrettyJsonValidate(PrettyJsonBaseCommand, sublime_plugin.TextCommand):
self.json_loads(self.view.substr(selection)) self.json_loads(self.view.substr(selection))
sublime.status_message("JSON is Valid") sublime.status_message("JSON is Valid")
except Exception as ex: except Exception as ex:
self.show_exception(msg=ex) self.show_exception(region=region, msg=ex)
sublime.message_dialog("Invalid JSON")
class PrettyJsonCommand(PrettyJsonBaseCommand, sublime_plugin.TextCommand): class PrettyJsonCommand(PrettyJsonBaseCommand, sublime_plugin.TextCommand):
""" Pretty Print JSON """ """
Description: Pretty Print JSON
"""
def run(self, edit): def run(self, edit):
self.view.erase_regions("json_errors") self.view.erase_regions("json_errors")
@ -231,6 +262,7 @@ class PrettyJsonCommand(PrettyJsonBaseCommand, sublime_plugin.TextCommand):
continue continue
elif region.empty() and s.get("use_entire_file_if_no_selection", True): elif region.empty() and s.get("use_entire_file_if_no_selection", True):
selection = sublime.Region(0, self.view.size()) selection = sublime.Region(0, self.view.size())
region = sublime.Region(0, self.view.size())
selected_entire_file = True selected_entire_file = True
else: else:
selection = region selection = region
@ -251,33 +283,36 @@ class PrettyJsonCommand(PrettyJsonBaseCommand, sublime_plugin.TextCommand):
except Exception as ex: except Exception as ex:
try: try:
amount_of_single_quotes = re.findall(r"(\'[^\']+\'?)", selection_text) amount_of_single_quotes = re.findall(
amount_of_double_quotes = re.findall(r"(\"[^\"]+\"?)", selection_text) r"(\'[^\']+\'?)", selection_text
)
amount_of_double_quotes = re.findall(
r"(\"[^\"]+\"?)", selection_text
)
if len(amount_of_single_quotes) >= len(amount_of_double_quotes): if len(amount_of_single_quotes) >= len(amount_of_double_quotes):
selection_text_modified = re.sub(r"(?:\'([^\']+)\'?)", r'"\1"', selection_text) selection_text_modified = re.sub(
r"(?:\'([^\']+)\'?)", r'"\1"', selection_text
)
obj = self.json_loads(selection_text_modified) obj = self.json_loads(selection_text_modified)
json_text = self.json_dumps(obj) json_text = self.json_dumps(obj)
if not selected_entire_file and s.get("reindent_block", False): if not selected_entire_file and s.get("reindent_block", False):
json_text = self.reindent(json_text, selection) json_text = self.reindent(json_text, selection)
self.view.replace(edit, selection, json_text) self.view.replace(edit, selection, json_text)
if selected_entire_file: if selected_entire_file:
self.syntax_to_json() self.syntax_to_json()
else: else:
self.show_exception(msg=ex) self.show_exception(region=region, msg=ex)
except Exception as ex: except Exception as ex:
self.show_exception(msg=ex) self.show_exception(region=region, msg=ex)
class PrettyJsonAndSortCommand(PrettyJsonCommand, sublime_plugin.TextCommand): class PrettyJsonAndSortCommand(PrettyJsonCommand, sublime_plugin.TextCommand):
"""
""" Pretty print json with forced sorting """ Description: Pretty print json with forced sorting
"""
def run(self, edit): def run(self, edit):
PrettyJsonBaseCommand.force_sorting = True PrettyJsonBaseCommand.force_sorting = True
@ -286,18 +321,19 @@ class PrettyJsonAndSortCommand(PrettyJsonCommand, sublime_plugin.TextCommand):
class UnPrettyJsonCommand(PrettyJsonBaseCommand, sublime_plugin.TextCommand): class UnPrettyJsonCommand(PrettyJsonBaseCommand, sublime_plugin.TextCommand):
"""
""" Compress/minify JSON - it makes json as one-liner """ Description: Compress/minify JSON - it makes json as one-liner
"""
def run(self, edit): def run(self, edit):
self.view.erase_regions("json_errors") self.view.erase_regions("json_errors")
for region in self.view.sel(): for region in self.view.sel():
selected_entire_file = False selected_entire_file = False
# If no selection, use the entire file as the selection # If no selection, use the entire file as the selection
if region.empty() and s.get("use_entire_file_if_no_selection", True): if region.empty() and s.get("use_entire_file_if_no_selection", True):
selection = sublime.Region(0, self.view.size()) selection = sublime.Region(0, self.view.size())
region = sublime.Region(0, self.view.size())
selected_entire_file = True selected_entire_file = True
else: else:
selection = region selection = region
@ -307,15 +343,15 @@ class UnPrettyJsonCommand(PrettyJsonBaseCommand, sublime_plugin.TextCommand):
self.view.replace(edit, selection, self.json_dumps_minified(obj)) self.view.replace(edit, selection, self.json_dumps_minified(obj))
if selected_entire_file: if selected_entire_file:
self.syntax_to_xml() self.syntax_to_json()
except Exception as ex: except Exception as ex:
self.show_exception(msg=ex) self.show_exception(region=region, msg=ex)
class JqPrettyJson(sublime_plugin.WindowCommand): class JqPrettyJson(sublime_plugin.WindowCommand):
""" """
Allows work with ./jq Description: Allows work with ./jq
""" """
def run(self): def run(self):
@ -351,13 +387,6 @@ class JqPrettyJson(sublime_plugin.WindowCommand):
) )
raw_json = self.get_content() raw_json = self.get_content()
if SUBLIME_MAJOR_VERSION < 3:
if sys.platform != "win32":
out, err = p.communicate(bytes(raw_json))
else:
out, err = p.communicate(unicode(raw_json).encode("utf-8"))
else:
out, err = p.communicate(bytes(raw_json, "utf-8")) out, err = p.communicate(bytes(raw_json, "utf-8"))
output = out.decode("UTF-8").replace(os.linesep, "\n").strip() output = out.decode("UTF-8").replace(os.linesep, "\n").strip()
if output: if output:
@ -372,7 +401,7 @@ class JqPrettyJson(sublime_plugin.WindowCommand):
class JsonToXml(PrettyJsonBaseCommand, sublime_plugin.TextCommand): class JsonToXml(PrettyJsonBaseCommand, sublime_plugin.TextCommand):
""" """
converts Json to XML Description: converts Json to XML
""" """
def run(self, edit): def run(self, edit):
@ -396,9 +425,6 @@ class JsonToXml(PrettyJsonBaseCommand, sublime_plugin.TextCommand):
xml_string = "<?xml version='1.0' encoding='UTF-8' ?>\n" xml_string = "<?xml version='1.0' encoding='UTF-8' ?>\n"
if SUBLIME_MAJOR_VERSION < 3:
self.indent_for_26(root)
rtn = ElementTree.tostring(root, "utf-8") rtn = ElementTree.tostring(root, "utf-8")
if type(rtn) is bytes: if type(rtn) is bytes:
@ -406,17 +432,9 @@ class JsonToXml(PrettyJsonBaseCommand, sublime_plugin.TextCommand):
xml_string += rtn xml_string += rtn
# for some reason python 2.6 shipped with ST2
# does not have pyexpat
if SUBLIME_MAJOR_VERSION >= 3:
xml_string = minidom.parseString(xml_string).toprettyxml(
encoding="UTF-8"
)
if type(xml_string) is bytes: if type(xml_string) is bytes:
xml_string = xml_string.decode("utf-8") xml_string = xml_string.decode("utf-8")
if not selected_entire_file and s.get("reindent_block", False): if not selected_entire_file and s.get("reindent_block", False):
xml_string = self.reindent(xml_string, selection) xml_string = self.reindent(xml_string, selection)
@ -426,23 +444,7 @@ class JsonToXml(PrettyJsonBaseCommand, sublime_plugin.TextCommand):
self.syntax_to_xml() self.syntax_to_xml()
except Exception as ex: except Exception as ex:
self.show_exception(msg=ex) self.show_exception(region=region, msg=ex)
def indent_for_26(self, elem, level=0):
""" intent of ElementTree in case it's py26 without minidom/pyexpat """
i = "\n" + level * " "
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = i + " "
if not elem.tail or not elem.tail.strip():
elem.tail = i
for elem in elem:
self.indent_for_26(elem, level + 1)
if not elem.tail or not elem.tail.strip():
elem.tail = i
else:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i
def traverse(self, element, json_dict): def traverse(self, element, json_dict):
""" recursive traverse through dict and build xml tree """ """ recursive traverse through dict and build xml tree """
@ -466,7 +468,7 @@ class JsonToXml(PrettyJsonBaseCommand, sublime_plugin.TextCommand):
class JqPrettyJsonOut(sublime_plugin.TextCommand): class JqPrettyJsonOut(sublime_plugin.TextCommand):
def run(self, edit, jq_output=""): def run(self, edit, jq_output=str()):
self.view.insert(edit, 0, jq_output) self.view.insert(edit, 0, jq_output)
@ -476,13 +478,12 @@ class PrettyJsonGotoSymbolCommand(PrettyJsonBaseCommand, sublime_plugin.TextComm
self.goto_items = [] self.goto_items = []
content = self.view.substr(sublime.Region(0, self.view.size())) content = self.view.substr(sublime.Region(0, self.view.size()))
try: try:
json_data = self.json_loads(content) json_data = self.json_loads(content)
self.generate_items(json_data, "") self.generate_items(json_data, "")
sublime.active_window().show_quick_panel(self.items, self.goto) sublime.active_window().show_quick_panel(self.items, self.goto)
except Exception as ex: except Exception as ex:
self.show_exception(msg=ex) self.show_exception(region=None, msg=ex)
def generate_items(self, json_data, root_key): def generate_items(self, json_data, root_key):
if isinstance(json_data, OrderedDict): if isinstance(json_data, OrderedDict):

View File

@ -27,8 +27,8 @@ class PrettyJsonLintListener(sublime_plugin.EventListener, PrettyJsonBaseCommand
try: try:
self.json_loads(json_content) self.json_loads(json_content)
except Exception: except Exception as ex:
self.show_exception() self.show_exception(msg=ex)
class PrettyJsonAutoPrettyOnSaveListener(sublime_plugin.EventListener): class PrettyJsonAutoPrettyOnSaveListener(sublime_plugin.EventListener):

28
phantom.css Normal file
View File

@ -0,0 +1,28 @@
div.error-arrow {
border-top: 0.4rem solid transparent;
border-left: 0.5rem solid color(var(--redish) blend(var(--background) 30%));
width: 0;
height: 0;
}
div.container {
margin: 0;
border-radius: 0 0.2rem 0.2rem 0.2rem;
}
div.content {
padding: 0.4rem 0.4rem 0.4rem 0.7rem;
}
div.content p {
margin: 0.2rem;
}
div.toolbar {
padding: 0.2rem 0.7rem 0.2rem 0.7rem;
}
div.toolbar a {
text-decoration: none
}
html.dark div.toolbar {
background-color: #00000018;
}
html.light div.toolbar {
background-color: #ffffff18;
}