83 | # The parser is relatively picky, and requires balanced tags for most 84 | # elements. However, elements belonging to the following group are 85 | # automatically closed: P, LI, TR, TH, and TD. In addition, the 86 | # parser automatically inserts end tags immediately after the start 87 | # tag, and ignores any end tags for the following group: IMG, HR, 88 | # META, and LINK. 89 | # 90 | # @keyparam builder Optional builder object. If omitted, the parser 91 | # uses the standard elementtree builder. 92 | # @keyparam encoding Optional character encoding, if known. If omitted, 93 | # the parser looks for META tags inside the document. If no tags 94 | # are found, the parser defaults to ISO-8859-1. Note that if your 95 | # document uses a non-ASCII compatible encoding, you must decode 96 | # the document before parsing. 97 | # 98 | # @see elementtree.ElementTree 99 | 100 | class HTMLTreeBuilder(HTMLParser): 101 | 102 | # FIXME: shouldn't this class be named Parser, not Builder? 103 | 104 | def __init__(self, builder=None, encoding=None): 105 | self.__stack = [] 106 | if builder is None: 107 | builder = ElementTree.TreeBuilder() 108 | self.__builder = builder 109 | self.encoding = encoding or "iso-8859-1" 110 | HTMLParser.__init__(self) 111 | 112 | ## 113 | # Flushes parser buffers, and return the root element. 114 | # 115 | # @return An Element instance. 116 | 117 | def close(self): 118 | HTMLParser.close(self) 119 | return self.__builder.close() 120 | 121 | ## 122 | # (Internal) Handles start tags. 123 | 124 | def handle_starttag(self, tag, attrs): 125 | if tag == "meta": 126 | # look for encoding directives 127 | http_equiv = content = None 128 | for k, v in attrs: 129 | if k == "http-equiv": 130 | http_equiv = string.lower(v) 131 | elif k == "content": 132 | content = v 133 | if http_equiv == "content-type" and content: 134 | # use mimetools to parse the http header 135 | header = mimetools.Message( 136 | StringIO.StringIO("%s: %s\n\n" % (http_equiv, content)) 137 | ) 138 | encoding = header.getparam("charset") 139 | if encoding: 140 | self.encoding = encoding 141 | if tag in AUTOCLOSE: 142 | if self.__stack and self.__stack[-1] == tag: 143 | self.handle_endtag(tag) 144 | self.__stack.append(tag) 145 | attrib = {} 146 | if attrs: 147 | for k, v in attrs: 148 | attrib[string.lower(k)] = v 149 | self.__builder.start(tag, attrib) 150 | if tag in IGNOREEND: 151 | self.__stack.pop() 152 | self.__builder.end(tag) 153 | 154 | ## 155 | # (Internal) Handles end tags. 156 | 157 | def handle_endtag(self, tag): 158 | if tag in IGNOREEND: 159 | return 160 | lasttag = self.__stack.pop() 161 | if tag != lasttag and lasttag in AUTOCLOSE: 162 | self.handle_endtag(lasttag) 163 | self.__builder.end(tag) 164 | 165 | ## 166 | # (Internal) Handles character references. 167 | 168 | def handle_charref(self, char): 169 | if char[:1] == "x": 170 | char = int(char[1:], 16) 171 | else: 172 | char = int(char) 173 | if 0 <= char < 128: 174 | self.__builder.data(chr(char)) 175 | else: 176 | self.__builder.data(unichr(char)) 177 | 178 | ## 179 | # (Internal) Handles entity references. 180 | 181 | def handle_entityref(self, name): 182 | entity = htmlentitydefs.entitydefs.get(name) 183 | if entity: 184 | if len(entity) == 1: 185 | entity = ord(entity) 186 | else: 187 | entity = int(entity[2:-1]) 188 | if 0 <= entity < 128: 189 | self.__builder.data(chr(entity)) 190 | else: 191 | self.__builder.data(unichr(entity)) 192 | else: 193 | self.unknown_entityref(name) 194 | 195 | ## 196 | # (Internal) Handles character data. 197 | 198 | def handle_data(self, data): 199 | if isinstance(data, type('')) and is_not_ascii(data): 200 | # convert to unicode, but only if necessary 201 | data = unicode(data, self.encoding, "ignore") 202 | self.__builder.data(data) 203 | 204 | ## 205 | # (Hook) Handles unknown entity references. The default action 206 | # is to ignore unknown entities. 207 | 208 | def unknown_entityref(self, name): 209 | pass # ignore by default; override if necessary 210 | 211 | ## 212 | # An alias for the HTMLTreeBuilder class. 213 | 214 | TreeBuilder = HTMLTreeBuilder 215 | 216 | ## 217 | # Parse an HTML document or document fragment. 218 | # 219 | # @param source A filename or file object containing HTML data. 220 | # @param encoding Optional character encoding, if known. If omitted, 221 | # the parser looks for META tags inside the document. If no tags 222 | # are found, the parser defaults to ISO-8859-1. 223 | # @return An ElementTree instance 224 | 225 | def parse(source, encoding=None): 226 | return ElementTree.parse(source, HTMLTreeBuilder(encoding=encoding)) 227 | 228 | if __name__ == "__main__": 229 | import sys 230 | ElementTree.dump(parse(open(sys.argv[1]))) 231 | -------------------------------------------------------------------------------- /xdebug/elementtree/SgmlopXMLTreeBuilder.py: -------------------------------------------------------------------------------- 1 | # 2 | # ElementTree 3 | # $Id$ 4 | # 5 | # A simple XML tree builder, based on the sgmlop library. 6 | # 7 | # Note that this version does not support namespaces. This may be 8 | # changed in future versions. 9 | # 10 | # history: 11 | # 2004-03-28 fl created 12 | # 13 | # Copyright (c) 1999-2004 by Fredrik Lundh. All rights reserved. 14 | # 15 | # fredrik@pythonware.com 16 | # http://www.pythonware.com 17 | # 18 | # -------------------------------------------------------------------- 19 | # The ElementTree toolkit is 20 | # 21 | # Copyright (c) 1999-2004 by Fredrik Lundh 22 | # 23 | # By obtaining, using, and/or copying this software and/or its 24 | # associated documentation, you agree that you have read, understood, 25 | # and will comply with the following terms and conditions: 26 | # 27 | # Permission to use, copy, modify, and distribute this software and 28 | # its associated documentation for any purpose and without fee is 29 | # hereby granted, provided that the above copyright notice appears in 30 | # all copies, and that both that copyright notice and this permission 31 | # notice appear in supporting documentation, and that the name of 32 | # Secret Labs AB or the author not be used in advertising or publicity 33 | # pertaining to distribution of the software without specific, written 34 | # prior permission. 35 | # 36 | # SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD 37 | # TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- 38 | # ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR 39 | # BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY 40 | # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, 41 | # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS 42 | # ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE 43 | # OF THIS SOFTWARE. 44 | # -------------------------------------------------------------------- 45 | 46 | ## 47 | # Tools to build element trees from XML, based on the SGMLOP parser. 48 | #
49 | # The current version does not support XML namespaces. 50 | #
51 | # This tree builder requires the sgmlop extension module 52 | # (available from 53 | # http://effbot.org/downloads). 54 | ## 55 | 56 | import ElementTree 57 | 58 | ## 59 | # ElementTree builder for XML source data, based on the SGMLOP parser. 60 | # 61 | # @see elementtree.ElementTree 62 | 63 | class TreeBuilder: 64 | 65 | def __init__(self, html=0): 66 | try: 67 | import sgmlop 68 | except ImportError: 69 | raise RuntimeError("sgmlop parser not available") 70 | self.__builder = ElementTree.TreeBuilder() 71 | if html: 72 | import htmlentitydefs 73 | self.entitydefs.update(htmlentitydefs.entitydefs) 74 | self.__parser = sgmlop.XMLParser() 75 | self.__parser.register(self) 76 | 77 | ## 78 | # Feeds data to the parser. 79 | # 80 | # @param data Encoded data. 81 | 82 | def feed(self, data): 83 | self.__parser.feed(data) 84 | 85 | ## 86 | # Finishes feeding data to the parser. 87 | # 88 | # @return An element structure. 89 | # @defreturn Element 90 | 91 | def close(self): 92 | self.__parser.close() 93 | self.__parser = None 94 | return self.__builder.close() 95 | 96 | def finish_starttag(self, tag, attrib): 97 | self.__builder.start(tag, attrib) 98 | 99 | def finish_endtag(self, tag): 100 | self.__builder.end(tag) 101 | 102 | def handle_data(self, data): 103 | self.__builder.data(data) 104 | -------------------------------------------------------------------------------- /xdebug/elementtree/SimpleXMLTreeBuilder.py: -------------------------------------------------------------------------------- 1 | # 2 | # ElementTree 3 | # $Id: SimpleXMLTreeBuilder.py 1862 2004-06-18 07:31:02Z Fredrik $ 4 | # 5 | # A simple XML tree builder, based on Python's xmllib 6 | # 7 | # Note that due to bugs in xmllib, this builder does not fully support 8 | # namespaces (unqualified attributes are put in the default namespace, 9 | # instead of being left as is). Run this module as a script to find 10 | # out if this affects your Python version. 11 | # 12 | # history: 13 | # 2001-10-20 fl created 14 | # 2002-05-01 fl added namespace support for xmllib 15 | # 2002-08-17 fl added xmllib sanity test 16 | # 17 | # Copyright (c) 1999-2004 by Fredrik Lundh. All rights reserved. 18 | # 19 | # fredrik@pythonware.com 20 | # http://www.pythonware.com 21 | # 22 | # -------------------------------------------------------------------- 23 | # The ElementTree toolkit is 24 | # 25 | # Copyright (c) 1999-2004 by Fredrik Lundh 26 | # 27 | # By obtaining, using, and/or copying this software and/or its 28 | # associated documentation, you agree that you have read, understood, 29 | # and will comply with the following terms and conditions: 30 | # 31 | # Permission to use, copy, modify, and distribute this software and 32 | # its associated documentation for any purpose and without fee is 33 | # hereby granted, provided that the above copyright notice appears in 34 | # all copies, and that both that copyright notice and this permission 35 | # notice appear in supporting documentation, and that the name of 36 | # Secret Labs AB or the author not be used in advertising or publicity 37 | # pertaining to distribution of the software without specific, written 38 | # prior permission. 39 | # 40 | # SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD 41 | # TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- 42 | # ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR 43 | # BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY 44 | # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, 45 | # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS 46 | # ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE 47 | # OF THIS SOFTWARE. 48 | # -------------------------------------------------------------------- 49 | 50 | ## 51 | # Tools to build element trees from XML files, using xmllib. 52 | # This module can be used instead of the standard tree builder, for 53 | # Python versions where "expat" is not available (such as 1.5.2). 54 | #
55 | # Note that due to bugs in xmllib, the namespace support is
56 | # not reliable (you can run the module as a script to find out exactly
57 | # how unreliable it is on your Python version).
58 | ##
59 |
60 | import xmllib, string
61 |
62 | import ElementTree
63 |
64 | ##
65 | # ElementTree builder for XML source data.
66 | #
67 | # @see elementtree.ElementTree
68 |
69 | class TreeBuilder(xmllib.XMLParser):
70 |
71 | def __init__(self, html=0):
72 | self.__builder = ElementTree.TreeBuilder()
73 | if html:
74 | import htmlentitydefs
75 | self.entitydefs.update(htmlentitydefs.entitydefs)
76 | xmllib.XMLParser.__init__(self)
77 |
78 | ##
79 | # Feeds data to the parser.
80 | #
81 | # @param data Encoded data.
82 |
83 | def feed(self, data):
84 | xmllib.XMLParser.feed(self, data)
85 |
86 | ##
87 | # Finishes feeding data to the parser.
88 | #
89 | # @return An element structure.
90 | # @defreturn Element
91 |
92 | def close(self):
93 | xmllib.XMLParser.close(self)
94 | return self.__builder.close()
95 |
96 | def handle_data(self, data):
97 | self.__builder.data(data)
98 |
99 | handle_cdata = handle_data
100 |
101 | def unknown_starttag(self, tag, attrs):
102 | attrib = {}
103 | for key, value in attrs.items():
104 | attrib[fixname(key)] = value
105 | self.__builder.start(fixname(tag), attrib)
106 |
107 | def unknown_endtag(self, tag):
108 | self.__builder.end(fixname(tag))
109 |
110 |
111 | def fixname(name, split=string.split):
112 | # xmllib in 2.0 and later provides limited (and slightly broken)
113 | # support for XML namespaces.
114 | if " " not in name:
115 | return name
116 | return "{%s}%s" % tuple(split(name, " ", 1))
117 |
118 |
119 | if __name__ == "__main__":
120 | import sys
121 | # sanity check: look for known namespace bugs in xmllib
122 | p = TreeBuilder()
123 | text = """\
124 |
52 | # The current version does not provide built-in support for 53 | # namespaces. To create files using namespaces, you have to provide 54 | # "xmlns" attributes and explicitly add prefixes to tags and 55 | # attributes. 56 | # 57 | #
61 | # 62 | # from elementtree.SimpleXMLWriter import XMLWriter 63 | # import sys 64 | # 65 | # w = XMLWriter(sys.stdout) 66 | # 67 | # html = w.start("html") 68 | # 69 | # w.start("head") 70 | # w.element("title", "my document") 71 | # w.element("meta", name="generator", value="my application 1.0") 72 | # w.end() 73 | # 74 | # w.start("body") 75 | # w.element("h1", "this is a heading") 76 | # w.element("p", "this is a paragraph") 77 | # 78 | # w.start("p") 79 | # w.data("this is ") 80 | # w.element("b", "bold") 81 | # w.data(" and ") 82 | # w.element("i", "italic") 83 | # w.data(".") 84 | # w.end("p") 85 | # 86 | # w.close(html) 87 | #88 | ## 89 | 90 | import re, sys, string 91 | 92 | try: 93 | unicode("") 94 | except NameError: 95 | def encode(s, encoding): 96 | # 1.5.2: application must use the right encoding 97 | return s 98 | _escape = re.compile(r"[&<>\"\x80-\xff]+") # 1.5.2 99 | else: 100 | def encode(s, encoding): 101 | return s.encode(encoding) 102 | _escape = re.compile(eval(r'u"[&<>\"\u0080-\uffff]+"')) 103 | 104 | def encode_entity(text, pattern=_escape): 105 | # map reserved and non-ascii characters to numerical entities 106 | def escape_entities(m): 107 | out = [] 108 | for char in m.group(): 109 | out.append("%d;" % ord(char)) 110 | return string.join(out, "") 111 | return encode(pattern.sub(escape_entities, text), "ascii") 112 | 113 | del _escape 114 | 115 | # 116 | # the following functions assume an ascii-compatible encoding 117 | # (or "utf-16") 118 | 119 | def escape_cdata(s, encoding=None, replace=string.replace): 120 | s = replace(s, "&", "&") 121 | s = replace(s, "<", "<") 122 | s = replace(s, ">", ">") 123 | if encoding: 124 | try: 125 | return encode(s, encoding) 126 | except UnicodeError: 127 | return encode_entity(s) 128 | return s 129 | 130 | def escape_attrib(s, encoding=None, replace=string.replace): 131 | s = replace(s, "&", "&") 132 | s = replace(s, "'", "'") 133 | s = replace(s, "\"", """) 134 | s = replace(s, "<", "<") 135 | s = replace(s, ">", ">") 136 | if encoding: 137 | try: 138 | return encode(s, encoding) 139 | except UnicodeError: 140 | return encode_entity(s) 141 | return s 142 | 143 | ## 144 | # XML writer class. 145 | # 146 | # @param file A file or file-like object. This object must implement 147 | # a write method that takes an 8-bit string. 148 | # @param encoding Optional encoding. 149 | 150 | class XMLWriter: 151 | 152 | def __init__(self, file, encoding="us-ascii"): 153 | if not hasattr(file, "write"): 154 | file = open(file, "w") 155 | self.__write = file.write 156 | if hasattr(file, "flush"): 157 | self.flush = file.flush 158 | self.__open = 0 # true if start tag is open 159 | self.__tags = [] 160 | self.__data = [] 161 | self.__encoding = encoding 162 | 163 | def __flush(self): 164 | # flush internal buffers 165 | if self.__open: 166 | self.__write(">") 167 | self.__open = 0 168 | if self.__data: 169 | data = string.join(self.__data, "") 170 | self.__write(escape_cdata(data, self.__encoding)) 171 | self.__data = [] 172 | 173 | ## 174 | # Writes an XML declaration. 175 | 176 | def declaration(self): 177 | encoding = self.__encoding 178 | if encoding == "us-ascii" or encoding == "utf-8": 179 | self.__write("\n") 180 | else: 181 | self.__write("\n" % encoding) 182 | 183 | ## 184 | # Opens a new element. Attributes can be given as keyword 185 | # arguments, or as a string/string dictionary. You can pass in 186 | # 8-bit strings or Unicode strings; the former are assumed to use 187 | # the encoding passed to the constructor. The method returns an 188 | # opaque identifier that can be passed to the close method, 189 | # to close all open elements up to and including this one. 190 | # 191 | # @param tag Element tag. 192 | # @param attrib Attribute dictionary. Alternatively, attributes 193 | # can be given as keyword arguments. 194 | # @return An element identifier. 195 | 196 | def start(self, tag, attrib={}, **extra): 197 | self.__flush() 198 | tag = escape_cdata(tag, self.__encoding) 199 | self.__data = [] 200 | self.__tags.append(tag) 201 | self.__write("<%s" % tag) 202 | if attrib or extra: 203 | attrib = attrib.copy() 204 | attrib.update(extra) 205 | attrib = attrib.items() 206 | attrib.sort() 207 | for k, v in attrib: 208 | k = escape_cdata(k, self.__encoding) 209 | v = escape_attrib(v, self.__encoding) 210 | self.__write(" %s=\"%s\"" % (k, v)) 211 | self.__open = 1 212 | return len(self.__tags)-1 213 | 214 | ## 215 | # Adds a comment to the output stream. 216 | # 217 | # @param comment Comment text, as an 8-bit string or Unicode string. 218 | 219 | def comment(self, comment): 220 | self.__flush() 221 | self.__write("\n" % escape_cdata(comment, self.__encoding)) 222 | 223 | ## 224 | # Adds character data to the output stream. 225 | # 226 | # @param text Character data, as an 8-bit string or Unicode string. 227 | 228 | def data(self, text): 229 | self.__data.append(text) 230 | 231 | ## 232 | # Closes the current element (opened by the most recent call to 233 | # start). 234 | # 235 | # @param tag Element tag. If given, the tag must match the start 236 | # tag. If omitted, the current element is closed. 237 | 238 | def end(self, tag=None): 239 | if tag: 240 | assert self.__tags, "unbalanced end(%s)" % tag 241 | assert escape_cdata(tag, self.__encoding) == self.__tags[-1],\ 242 | "expected end(%s), got %s" % (self.__tags[-1], tag) 243 | else: 244 | assert self.__tags, "unbalanced end()" 245 | tag = self.__tags.pop() 246 | if self.__data: 247 | self.__flush() 248 | elif self.__open: 249 | self.__open = 0 250 | self.__write(" />") 251 | return 252 | self.__write("%s>" % tag) 253 | 254 | ## 255 | # Closes open elements, up to (and including) the element identified 256 | # by the given identifier. 257 | # 258 | # @param id Element identifier, as returned by the start method. 259 | 260 | def close(self, id): 261 | while len(self.__tags) > id: 262 | self.end() 263 | 264 | ## 265 | # Adds an entire element. This is the same as calling start, 266 | # data, and end in sequence. The text argument 267 | # can be omitted. 268 | 269 | def element(self, tag, text=None, attrib={}, **extra): 270 | apply(self.start, (tag, attrib), extra) 271 | if text: 272 | self.data(text) 273 | self.end() 274 | 275 | ## 276 | # Flushes the output stream. 277 | 278 | def flush(self): 279 | pass # replaced by the constructor 280 | -------------------------------------------------------------------------------- /xdebug/elementtree/TidyHTMLTreeBuilder.py: -------------------------------------------------------------------------------- 1 | # 2 | # ElementTree 3 | # $Id: TidyHTMLTreeBuilder.py 2304 2005-03-01 17:42:41Z fredrik $ 4 | # 5 | 6 | from elementtidy.TidyHTMLTreeBuilder import * 7 | -------------------------------------------------------------------------------- /xdebug/elementtree/TidyTools.py: -------------------------------------------------------------------------------- 1 | # 2 | # ElementTree 3 | # $Id: TidyTools.py 1862 2004-06-18 07:31:02Z Fredrik $ 4 | # 5 | # tools to run the "tidy" command on an HTML or XHTML file, and return 6 | # the contents as an XHTML element tree. 7 | # 8 | # history: 9 | # 2002-10-19 fl added to ElementTree library; added getzonebody function 10 | # 11 | # Copyright (c) 1999-2004 by Fredrik Lundh. All rights reserved. 12 | # 13 | # fredrik@pythonware.com 14 | # http://www.pythonware.com 15 | # 16 | 17 | ## 18 | # Tools to build element trees from HTML, using the external tidy 19 | # utility. 20 | ## 21 | 22 | import glob, string, os, sys 23 | 24 | from ElementTree import ElementTree, Element 25 | 26 | NS_XHTML = "{http://www.w3.org/1999/xhtml}" 27 | 28 | ## 29 | # Convert an HTML or HTML-like file to XHTML, using the tidy 30 | # command line utility. 31 | # 32 | # @param file Filename. 33 | # @param new_inline_tags An optional list of valid but non-standard 34 | # inline tags. 35 | # @return An element tree, or None if not successful. 36 | 37 | def tidy(file, new_inline_tags=None): 38 | 39 | command = ["tidy", "-qn", "-asxml"] 40 | 41 | if new_inline_tags: 42 | command.append("--new-inline-tags") 43 | command.append(string.join(new_inline_tags, ",")) 44 | 45 | # FIXME: support more tidy options! 46 | 47 | # convert 48 | os.system( 49 | "%s %s >%s.out 2>%s.err" % (string.join(command), file, file, file) 50 | ) 51 | # check that the result is valid XML 52 | try: 53 | tree = ElementTree() 54 | tree.parse(file + ".out") 55 | except: 56 | print "*** %s:%s" % sys.exc_info()[:2] 57 | print ("*** %s is not valid XML " 58 | "(check %s.err for info)" % (file, file)) 59 | tree = None 60 | else: 61 | if os.path.isfile(file + ".out"): 62 | os.remove(file + ".out") 63 | if os.path.isfile(file + ".err"): 64 | os.remove(file + ".err") 65 | 66 | return tree 67 | 68 | ## 69 | # Get document body from a an HTML or HTML-like file. This function 70 | # uses the tidy function to convert HTML to XHTML, and cleans 71 | # up the resulting XML tree. 72 | # 73 | # @param file Filename. 74 | # @return A body element, or None if not successful. 75 | 76 | def getbody(file, **options): 77 | # get clean body from text file 78 | 79 | # get xhtml tree 80 | try: 81 | tree = apply(tidy, (file,), options) 82 | if tree is None: 83 | return 84 | except IOError, v: 85 | print "***", v 86 | return None 87 | 88 | NS = NS_XHTML 89 | 90 | # remove namespace uris 91 | for node in tree.getiterator(): 92 | if node.tag.startswith(NS): 93 | node.tag = node.tag[len(NS):] 94 | 95 | body = tree.getroot().find("body") 96 | 97 | return body 98 | 99 | ## 100 | # Same as getbody, but turns plain text at the start of the 101 | # document into an H1 tag. This function can be used to parse zone 102 | # documents. 103 | # 104 | # @param file Filename. 105 | # @return A body element, or None if not successful. 106 | 107 | def getzonebody(file, **options): 108 | 109 | body = getbody(file, **options) 110 | if body is None: 111 | return 112 | 113 | if body.text and string.strip(body.text): 114 | title = Element("h1") 115 | title.text = string.strip(body.text) 116 | title.tail = "\n\n" 117 | body.insert(0, title) 118 | 119 | body.text = None 120 | 121 | return body 122 | 123 | if __name__ == "__main__": 124 | 125 | import sys 126 | for arg in sys.argv[1:]: 127 | for file in glob.glob(arg): 128 | print file, "...", tidy(file) 129 | -------------------------------------------------------------------------------- /xdebug/elementtree/XMLTreeBuilder.py: -------------------------------------------------------------------------------- 1 | # 2 | # ElementTree 3 | # $Id: XMLTreeBuilder.py 2305 2005-03-01 17:43:09Z fredrik $ 4 | # 5 | # an XML tree builder 6 | # 7 | # history: 8 | # 2001-10-20 fl created 9 | # 2002-05-01 fl added namespace support for xmllib 10 | # 2002-07-27 fl require expat (1.5.2 code can use SimpleXMLTreeBuilder) 11 | # 2002-08-17 fl use tag/attribute name memo cache 12 | # 2002-12-04 fl moved XMLTreeBuilder to the ElementTree module 13 | # 14 | # Copyright (c) 1999-2004 by Fredrik Lundh. All rights reserved. 15 | # 16 | # fredrik@pythonware.com 17 | # http://www.pythonware.com 18 | # 19 | # -------------------------------------------------------------------- 20 | # The ElementTree toolkit is 21 | # 22 | # Copyright (c) 1999-2004 by Fredrik Lundh 23 | # 24 | # By obtaining, using, and/or copying this software and/or its 25 | # associated documentation, you agree that you have read, understood, 26 | # and will comply with the following terms and conditions: 27 | # 28 | # Permission to use, copy, modify, and distribute this software and 29 | # its associated documentation for any purpose and without fee is 30 | # hereby granted, provided that the above copyright notice appears in 31 | # all copies, and that both that copyright notice and this permission 32 | # notice appear in supporting documentation, and that the name of 33 | # Secret Labs AB or the author not be used in advertising or publicity 34 | # pertaining to distribution of the software without specific, written 35 | # prior permission. 36 | # 37 | # SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD 38 | # TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- 39 | # ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR 40 | # BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY 41 | # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, 42 | # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS 43 | # ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE 44 | # OF THIS SOFTWARE. 45 | # -------------------------------------------------------------------- 46 | 47 | ## 48 | # Tools to build element trees from XML files. 49 | ## 50 | 51 | import ElementTree 52 | 53 | ## 54 | # (obsolete) ElementTree builder for XML source data, based on the 55 | # expat parser. 56 | #
57 | # This class is an alias for ElementTree.XMLTreeBuilder. New code 58 | # should use that version instead. 59 | # 60 | # @see elementtree.ElementTree 61 | 62 | class TreeBuilder(ElementTree.XMLTreeBuilder): 63 | pass 64 | 65 | ## 66 | # (experimental) An alternate builder that supports manipulation of 67 | # new elements. 68 | 69 | class FancyTreeBuilder(TreeBuilder): 70 | 71 | def __init__(self, html=0): 72 | TreeBuilder.__init__(self, html) 73 | self._parser.StartNamespaceDeclHandler = self._start_ns 74 | self._parser.EndNamespaceDeclHandler = self._end_ns 75 | self.namespaces = [] 76 | 77 | def _start(self, tag, attrib_in): 78 | elem = TreeBuilder._start(self, tag, attrib_in) 79 | self.start(elem) 80 | 81 | def _start_list(self, tag, attrib_in): 82 | elem = TreeBuilder._start_list(self, tag, attrib_in) 83 | self.start(elem) 84 | 85 | def _end(self, tag): 86 | elem = TreeBuilder._end(self, tag) 87 | self.end(elem) 88 | 89 | def _start_ns(self, prefix, value): 90 | self.namespaces.insert(0, (prefix, value)) 91 | 92 | def _end_ns(self, prefix): 93 | assert self.namespaces.pop(0)[0] == prefix, "implementation confused" 94 | 95 | ## 96 | # Hook method that's called when a new element has been opened. 97 | # May access the namespaces attribute. 98 | # 99 | # @param element The new element. The tag name and attributes are, 100 | # set, but it has no children, and the text and tail attributes 101 | # are still empty. 102 | 103 | def start(self, element): 104 | pass 105 | 106 | ## 107 | # Hook method that's called when a new element has been closed. 108 | # May access the namespaces attribute. 109 | # 110 | # @param element The new element. 111 | 112 | def end(self, element): 113 | pass 114 | -------------------------------------------------------------------------------- /xdebug/elementtree/__init__.py: -------------------------------------------------------------------------------- 1 | # $Id: __init__.py 1821 2004-06-03 16:57:49Z fredrik $ 2 | # elementtree package 3 | 4 | # -------------------------------------------------------------------- 5 | # The ElementTree toolkit is 6 | # 7 | # Copyright (c) 1999-2004 by Fredrik Lundh 8 | # 9 | # By obtaining, using, and/or copying this software and/or its 10 | # associated documentation, you agree that you have read, understood, 11 | # and will comply with the following terms and conditions: 12 | # 13 | # Permission to use, copy, modify, and distribute this software and 14 | # its associated documentation for any purpose and without fee is 15 | # hereby granted, provided that the above copyright notice appears in 16 | # all copies, and that both that copyright notice and this permission 17 | # notice appear in supporting documentation, and that the name of 18 | # Secret Labs AB or the author not be used in advertising or publicity 19 | # pertaining to distribution of the software without specific, written 20 | # prior permission. 21 | # 22 | # SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD 23 | # TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- 24 | # ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR 25 | # BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY 26 | # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, 27 | # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS 28 | # ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE 29 | # OF THIS SOFTWARE. 30 | # -------------------------------------------------------------------- 31 | -------------------------------------------------------------------------------- /xdebug/helper/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | # Get python version 4 | python_version = sys.version_info[:2] 5 | 6 | 7 | # Define helper class according to python version 8 | if (python_version <= (2, 6)): 9 | # Version 2.6 and below 10 | import helper_26 as H 11 | elif (python_version == (2, 7)): 12 | # Version 2.7 13 | from . import helper_27 as H 14 | else: 15 | # Version 3+ 16 | from . import helper as H 17 | 18 | 19 | # Modules to be imported from package when using * 20 | __all__ = ['H'] 21 | -------------------------------------------------------------------------------- /xdebug/helper/helper.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper module for Python version 3.0 and above 3 | - Ordered dictionaries 4 | - Encoding/decoding urls 5 | - Unicode/Bytes (for sending/receiving data from/to socket, base64) 6 | - Exception handling (except Exception as e) 7 | """ 8 | 9 | import base64 10 | from urllib.parse import unquote, quote 11 | from collections import OrderedDict 12 | 13 | 14 | def modulename(): 15 | return 'Helper module for Python version 3.0 and above' 16 | 17 | 18 | def url_decode(uri): 19 | return unquote(uri) 20 | 21 | 22 | def url_encode(uri): 23 | return quote(uri) 24 | 25 | 26 | def new_dictionary(): 27 | return OrderedDict() 28 | 29 | 30 | def dictionary_keys(dictionary): 31 | return list(dictionary.keys()) 32 | 33 | 34 | def dictionary_values(dictionary): 35 | return list(dictionary.values()) 36 | 37 | 38 | def data_read(data): 39 | # Convert bytes to string 40 | return data.decode('utf8') 41 | 42 | 43 | def data_write(data): 44 | # Convert string to bytes 45 | return bytes(data, 'utf8') 46 | 47 | 48 | def base64_decode(data): 49 | # Base64 returns decoded byte string, decode to convert to UTF8 string 50 | return base64.b64decode(data).decode('utf8') 51 | 52 | 53 | def base64_encode(data): 54 | # Base64 needs ascii input to encode, which returns Base64 byte string, decode to convert to UTF8 string 55 | return base64.b64encode(data.encode('ascii')).decode('utf8') 56 | 57 | 58 | def unicode_chr(code): 59 | return chr(code) 60 | 61 | 62 | def unicode_string(string): 63 | # Python 3.* uses unicode by default 64 | return string 65 | 66 | 67 | def is_digit(string): 68 | # Check if string is digit 69 | return isinstance(string, str) and string.isdigit() 70 | 71 | 72 | def is_number(value): 73 | return isinstance(value, int) 74 | -------------------------------------------------------------------------------- /xdebug/helper/helper_26.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper module for Python version 2.6 and below 3 | - Ordered dictionaries 4 | - Encoding/decoding urls 5 | - Unicode 6 | - Exception handling (except Exception, e) 7 | """ 8 | 9 | import base64 10 | from urllib import unquote, quote 11 | try: 12 | from ordereddict import OrderedDict 13 | except: 14 | pass 15 | 16 | 17 | def modulename(): 18 | return 'Helper module for Python version 2.6 and below' 19 | 20 | 21 | def url_decode(uri): 22 | return unquote(uri) 23 | 24 | 25 | def url_encode(uri): 26 | return quote(uri) 27 | 28 | 29 | def new_dictionary(): 30 | try: 31 | return OrderedDict() 32 | except: 33 | return {} 34 | 35 | 36 | def dictionary_keys(dictionary): 37 | return dictionary.keys() 38 | 39 | 40 | def dictionary_values(dictionary): 41 | return dictionary.values() 42 | 43 | 44 | def data_read(data): 45 | # Data for reading/receiving already a string in version 2.* 46 | return data 47 | 48 | 49 | def data_write(data): 50 | # Using string in version 2.* for sending/writing data 51 | return data 52 | 53 | 54 | def base64_decode(data): 55 | return base64.b64decode(data) 56 | 57 | 58 | def base64_encode(data): 59 | return base64.b64encode(data) 60 | 61 | 62 | def unicode_chr(code): 63 | return unichr(code) # noqa: F821 64 | 65 | 66 | def unicode_string(string): 67 | if isinstance(string, unicode): # noqa: F821 68 | return string 69 | return string.decode('utf8', 'replace') 70 | 71 | 72 | def is_digit(string): 73 | # Check if basestring (str, unicode) is digit 74 | return isinstance(string, basestring) and string.isdigit() # noqa: F821 75 | 76 | 77 | def is_number(value): 78 | return isinstance(value, (int, long)) # noqa: F821 79 | -------------------------------------------------------------------------------- /xdebug/helper/helper_27.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper module for Python version 2.7 3 | - Ordered dictionaries 4 | - Encoding/decoding urls 5 | - Unicode 6 | - Exception handling (except Exception as e) 7 | """ 8 | 9 | import base64 10 | from urllib import unquote, quote 11 | from collections import OrderedDict 12 | 13 | 14 | def modulename(): 15 | return 'Helper module for Python version 2.7' 16 | 17 | 18 | def url_decode(uri): 19 | return unquote(uri) 20 | 21 | 22 | def url_encode(uri): 23 | return quote(uri) 24 | 25 | 26 | def new_dictionary(): 27 | return OrderedDict() 28 | 29 | 30 | def dictionary_keys(dictionary): 31 | return list(dictionary.keys()) 32 | 33 | 34 | def dictionary_values(dictionary): 35 | return list(dictionary.values()) 36 | 37 | 38 | def data_read(data): 39 | # Data for reading/receiving already a string in version 2.* 40 | return data 41 | 42 | 43 | def data_write(data): 44 | # Using string in version 2.* for sending/writing data 45 | return data 46 | 47 | 48 | def base64_decode(data): 49 | return base64.b64decode(data) 50 | 51 | 52 | def base64_encode(data): 53 | return base64.b64encode(data) 54 | 55 | 56 | def unicode_chr(code): 57 | return unichr(code) # noqa: F821 58 | 59 | 60 | def unicode_string(string): 61 | if isinstance(string, unicode): # noqa: F821 62 | return string 63 | return string.decode('utf8', 'replace') 64 | 65 | 66 | def is_digit(string): 67 | # Check if basestring (str, unicode) is digit 68 | return isinstance(string, basestring) and string.isdigit() # noqa: F821 69 | 70 | 71 | def is_number(value): 72 | return isinstance(value, (int, long)) # noqa: F821 73 | -------------------------------------------------------------------------------- /xdebug/helper/ordereddict.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2009 Raymond Hettinger 2 | # 3 | # Permission is hereby granted, free of charge, to any person 4 | # obtaining a copy of this software and associated documentation files 5 | # (the "Software"), to deal in the Software without restriction, 6 | # including without limitation the rights to use, copy, modify, merge, 7 | # publish, distribute, sublicense, and/or sell copies of the Software, 8 | # and to permit persons to whom the Software is furnished to do so, 9 | # subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be 12 | # included in all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 16 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 18 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 19 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 21 | # OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | from UserDict import DictMixin 24 | 25 | class OrderedDict(dict, DictMixin): 26 | 27 | def __init__(self, *args, **kwds): 28 | if len(args) > 1: 29 | raise TypeError('expected at most 1 arguments, got %d' % len(args)) 30 | try: 31 | self.__end 32 | except AttributeError: 33 | self.clear() 34 | self.update(*args, **kwds) 35 | 36 | def clear(self): 37 | self.__end = end = [] 38 | end += [None, end, end] # sentinel node for doubly linked list 39 | self.__map = {} # key --> [key, prev, next] 40 | dict.clear(self) 41 | 42 | def __setitem__(self, key, value): 43 | if key not in self: 44 | end = self.__end 45 | curr = end[1] 46 | curr[2] = end[1] = self.__map[key] = [key, curr, end] 47 | dict.__setitem__(self, key, value) 48 | 49 | def __delitem__(self, key): 50 | dict.__delitem__(self, key) 51 | key, prev, next = self.__map.pop(key) 52 | prev[2] = next 53 | next[1] = prev 54 | 55 | def __iter__(self): 56 | end = self.__end 57 | curr = end[2] 58 | while curr is not end: 59 | yield curr[0] 60 | curr = curr[2] 61 | 62 | def __reversed__(self): 63 | end = self.__end 64 | curr = end[1] 65 | while curr is not end: 66 | yield curr[0] 67 | curr = curr[1] 68 | 69 | def popitem(self, last=True): 70 | if not self: 71 | raise KeyError('dictionary is empty') 72 | if last: 73 | key = reversed(self).next() 74 | else: 75 | key = iter(self).next() 76 | value = self.pop(key) 77 | return key, value 78 | 79 | def __reduce__(self): 80 | items = [[k, self[k]] for k in self] 81 | tmp = self.__map, self.__end 82 | del self.__map, self.__end 83 | inst_dict = vars(self).copy() 84 | self.__map, self.__end = tmp 85 | if inst_dict: 86 | return (self.__class__, (items,), inst_dict) 87 | return self.__class__, (items,) 88 | 89 | def keys(self): 90 | return list(self) 91 | 92 | setdefault = DictMixin.setdefault 93 | update = DictMixin.update 94 | pop = DictMixin.pop 95 | values = DictMixin.values 96 | items = DictMixin.items 97 | iterkeys = DictMixin.iterkeys 98 | itervalues = DictMixin.itervalues 99 | iteritems = DictMixin.iteritems 100 | 101 | def __repr__(self): 102 | if not self: 103 | return '%s()' % (self.__class__.__name__,) 104 | return '%s(%r)' % (self.__class__.__name__, self.items()) 105 | 106 | def copy(self): 107 | return self.__class__(self) 108 | 109 | @classmethod 110 | def fromkeys(cls, iterable, value=None): 111 | d = cls() 112 | for key in iterable: 113 | d[key] = value 114 | return d 115 | 116 | def __eq__(self, other): 117 | if isinstance(other, OrderedDict): 118 | if len(self) != len(other): 119 | return False 120 | for p, q in zip(self.items(), other.items()): 121 | if p != q: 122 | return False 123 | return True 124 | return dict.__eq__(self, other) 125 | 126 | def __ne__(self, other): 127 | return not self == other 128 | -------------------------------------------------------------------------------- /xdebug/load.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | 3 | import os 4 | 5 | # Settings variables 6 | try: 7 | from . import settings as S 8 | except: 9 | import settings as S 10 | 11 | # Load modules 12 | from .view import DATA_BREAKPOINT, DATA_CONTEXT, DATA_STACK, DATA_WATCH, TITLE_WINDOW_BREAKPOINT, TITLE_WINDOW_CONTEXT, TITLE_WINDOW_STACK, TITLE_WINDOW_WATCH, has_debug_view, render_regions, show_content 13 | from .util import load_breakpoint_data, load_watch_data 14 | from .log import clear_output, debug, info 15 | from .config import get_window_value, set_window_value, load_package_values, load_project_values 16 | 17 | 18 | def xdebug(): 19 | # Clear log file 20 | clear_output() 21 | if not S.PACKAGE_FOLDER: 22 | info('Unable to resolve current path for package.') 23 | info('==== Loading "%s" package ====' % S.PACKAGE_FOLDER) 24 | 25 | # Load config in package/project configuration 26 | load_package_values() 27 | load_project_values() 28 | 29 | # Load breakpoint data 30 | try: 31 | load_breakpoint_data() 32 | finally: 33 | # Render breakpoint markers 34 | render_regions() 35 | 36 | # Load watch data 37 | load_watch_data() 38 | 39 | # Clear/Reset content in debug windows 40 | if has_debug_view(TITLE_WINDOW_BREAKPOINT): 41 | show_content(DATA_BREAKPOINT) 42 | if has_debug_view(TITLE_WINDOW_CONTEXT): 43 | show_content(DATA_CONTEXT) 44 | if has_debug_view(TITLE_WINDOW_STACK): 45 | show_content(DATA_STACK) 46 | if has_debug_view(TITLE_WINDOW_WATCH): 47 | show_content(DATA_WATCH) 48 | 49 | # Check for conflicting packages 50 | if S.PACKAGE_FOLDER: 51 | # Get package list from Package Control 52 | packages = None 53 | try: 54 | packages = sublime.load_settings('Package Control.sublime-settings').get('installed_packages', []) 55 | except: 56 | pass 57 | # Make sure it is a list 58 | if not isinstance(packages, list): 59 | packages = [] 60 | # Get packages inside Package directory 61 | for package_name in os.listdir(sublime.packages_path()): 62 | if package_name not in packages: 63 | packages.append(package_name) 64 | # Strip .sublime-package of package name for comparison 65 | package_extension = '.sublime-package' 66 | current_package = S.PACKAGE_FOLDER 67 | if current_package.endswith(package_extension): 68 | current_package = current_package[:-len(package_extension)] 69 | # Search for other conflicting packages 70 | conflict = [] 71 | for package in packages: 72 | if package.endswith(package_extension): 73 | package = package[:-len(package_extension)] 74 | if (package.lower().count('xdebug') or package.lower().count('moai')) and package != current_package: 75 | conflict.append(package) 76 | # Show message if conflicting packages have been found 77 | if conflict: 78 | info('Conflicting packages detected.') 79 | debug(conflict) 80 | if not get_window_value('hide_conflict', False): 81 | sublime.error_message('The following package(s) could cause conflicts with "{package}":\n\n{other}\n\nPlease consider removing the package(s) above when experiencing any complications.' 82 | .format(package=S.PACKAGE_FOLDER, other='\n'.join(conflict))) 83 | set_window_value('hide_conflict', True) 84 | else: 85 | set_window_value('hide_conflict', False) 86 | -------------------------------------------------------------------------------- /xdebug/log.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | 3 | import logging 4 | import os 5 | 6 | # Settings variables 7 | try: 8 | from . import settings as S 9 | except: 10 | import settings as S 11 | 12 | # Config module 13 | from .config import get_value 14 | 15 | 16 | def clear_output(): 17 | # Clear previous output file and configure logging module 18 | output_file = os.path.join(sublime.packages_path(), 'User', S.FILE_LOG_OUTPUT) 19 | logging.basicConfig(filename=output_file, filemode='w', level=logging.DEBUG, format='[%(asctime)s] %(levelname)s - %(message)s', datefmt='%m/%d/%Y %I:%M:%S%p') 20 | 21 | 22 | def debug(message=None): 23 | if not get_value(S.KEY_DEBUG) or message is None: 24 | return 25 | # Write message to output file 26 | logging.debug(message) 27 | 28 | 29 | def info(message=None): 30 | if message is None: 31 | return 32 | # Write message to output file 33 | logging.info(message) 34 | -------------------------------------------------------------------------------- /xdebug/protocol.py: -------------------------------------------------------------------------------- 1 | import errno 2 | import re 3 | import socket 4 | import sys 5 | 6 | # Helper module 7 | try: 8 | from .helper import H 9 | except: 10 | from helper import H 11 | 12 | # Settings variables 13 | try: 14 | from . import settings as S 15 | except: 16 | import settings as S 17 | 18 | # Config module 19 | from .config import get_value 20 | 21 | # Log module 22 | from .log import debug 23 | 24 | # HTML entities 25 | try: 26 | from html.entities import name2codepoint 27 | except ImportError: 28 | from htmlentitydefs import name2codepoint 29 | 30 | # XML parser 31 | try: 32 | from xml.etree import cElementTree as ET 33 | except ImportError: 34 | try: 35 | from xml.etree import ElementTree as ET 36 | except ImportError: 37 | from .elementtree import ElementTree as ET 38 | try: 39 | from xml.parsers import expat # noqa: F401 40 | UNESCAPE_RESPONSE_DATA = True 41 | except ImportError: 42 | # Module xml.parsers.expat missing, using SimpleXMLTreeBuilder 43 | from .elementtree import SimpleXMLTreeBuilder 44 | ET.XMLTreeBuilder = SimpleXMLTreeBuilder.TreeBuilder 45 | UNESCAPE_RESPONSE_DATA = False 46 | 47 | 48 | ILLEGAL_XML_UNICODE_CHARACTERS = [ 49 | (0x00, 0x08), (0x0B, 0x0C), (0x0E, 0x1F), (0x7F, 0x84), 50 | (0x86, 0x9F), (0xD800, 0xDFFF), (0xFDD0, 0xFDDF), 51 | (0xFFFE, 0xFFFF), 52 | (0x1FFFE, 0x1FFFF), (0x2FFFE, 0x2FFFF), (0x3FFFE, 0x3FFFF), 53 | (0x4FFFE, 0x4FFFF), (0x5FFFE, 0x5FFFF), (0x6FFFE, 0x6FFFF), 54 | (0x7FFFE, 0x7FFFF), (0x8FFFE, 0x8FFFF), (0x9FFFE, 0x9FFFF), 55 | (0xAFFFE, 0xAFFFF), (0xBFFFE, 0xBFFFF), (0xCFFFE, 0xCFFFF), 56 | (0xDFFFE, 0xDFFFF), (0xEFFFE, 0xEFFFF), (0xFFFFE, 0xFFFFF), 57 | (0x10FFFE, 0x10FFFF)] 58 | 59 | ILLEGAL_XML_RANGES = [ 60 | '%s-%s' % (H.unicode_chr(low), H.unicode_chr(high)) 61 | for (low, high) in ILLEGAL_XML_UNICODE_CHARACTERS 62 | if low < sys.maxunicode 63 | ] 64 | 65 | ILLEGAL_XML_RE = re.compile(H.unicode_string('[%s]') % H.unicode_string('').join(ILLEGAL_XML_RANGES)) 66 | 67 | 68 | class Protocol(object): 69 | """ 70 | Class for connecting with debugger engine which uses DBGp protocol. 71 | """ 72 | 73 | # Maximum amount of data to be received at once by socket 74 | read_size = 1024 75 | 76 | def __init__(self): 77 | # Set host address to listen for response 78 | self.host = get_value(S.KEY_HOST, S.DEFAULT_HOST) 79 | # Set port number to listen for response 80 | self.port = get_value(S.KEY_PORT, S.DEFAULT_PORT) 81 | self.clear() 82 | 83 | def transaction_id(): 84 | """ 85 | Standard argument for sending commands, an unique numerical ID. 86 | """ 87 | def fget(self): 88 | self._transaction_id += 1 89 | return self._transaction_id 90 | 91 | def fset(self, value): 92 | self._transaction_id = value 93 | 94 | def fdel(self): 95 | self._transaction_id = 0 96 | return locals() 97 | 98 | # Transaction ID property 99 | transaction_id = property(**transaction_id()) 100 | 101 | def clear(self): 102 | """ 103 | Clear variables, reset transaction_id, close socket connection. 104 | """ 105 | self.buffer = '' 106 | self.connected = False 107 | self.listening = False 108 | del self.transaction_id 109 | try: 110 | self.socket.close() 111 | except: 112 | pass 113 | self.socket = None 114 | 115 | def unescape(self, string): 116 | """ 117 | Convert HTML entities and character references to ordinary characters. 118 | """ 119 | def convert(matches): 120 | text = matches.group(0) 121 | # Character reference 122 | if text[:2] == '': 123 | try: 124 | if text[:3] == '': 125 | return H.unicode_chr(int(text[3:-1], 16)) 126 | else: 127 | return H.unicode_chr(int(text[2:-1])) 128 | except ValueError: 129 | pass 130 | # Named entity 131 | else: 132 | try: 133 | # Following are not needed to be converted for XML 134 | if text[1:-1] in ('amp', 'apos', 'gt', 'lt', 'quot'): 135 | pass 136 | else: 137 | text = H.unicode_chr(name2codepoint[text[1:-1]]) 138 | except KeyError: 139 | pass 140 | return text 141 | return re.sub(r'?\w+;', convert, string) 142 | 143 | def read_until_null(self): 144 | """ 145 | Get response data from debugger engine. 146 | """ 147 | # Check socket connection 148 | if self.connected: 149 | # Get result data from debugger engine 150 | try: 151 | while '\x00' not in self.buffer: 152 | self.buffer += H.data_read(self.socket.recv(self.read_size)) 153 | data, self.buffer = self.buffer.split('\x00', 1) 154 | return data 155 | except: 156 | e = sys.exc_info()[1] 157 | raise ProtocolConnectionException(e) 158 | else: 159 | raise ProtocolConnectionException('Xdebug is not connected') 160 | 161 | def read_data(self): 162 | """ 163 | Get response data from debugger engine and verify length of response. 164 | """ 165 | # Verify length of response data 166 | length = self.read_until_null() 167 | message = self.read_until_null() 168 | if int(length) == len(message): 169 | return message 170 | else: 171 | raise ProtocolException('Length mismatch encountered while reading the Xdebug message') 172 | 173 | def read(self, return_string=False): 174 | """ 175 | Get response from debugger engine as XML document object. 176 | """ 177 | # Get result data from debugger engine and verify length of response 178 | data = self.read_data() 179 | 180 | # Show debug output 181 | debug('[Response data] %s' % data) 182 | 183 | # Return data string 184 | if return_string: 185 | return data 186 | 187 | # Remove special character quoting 188 | if UNESCAPE_RESPONSE_DATA: 189 | data = self.unescape(data) 190 | 191 | # Replace invalid XML characters 192 | data = ILLEGAL_XML_RE.sub('?', data) 193 | 194 | # Create XML document object 195 | document = ET.fromstring(data) 196 | return document 197 | 198 | def send(self, command, *args, **kwargs): 199 | """ 200 | Send command to the debugger engine according to DBGp protocol. 201 | """ 202 | # Expression is used for conditional and watch type breakpoints 203 | expression = None 204 | 205 | # Separate 'expression' from kwargs 206 | if 'expression' in kwargs: 207 | expression = kwargs['expression'] 208 | del kwargs['expression'] 209 | 210 | # Generate unique Transaction ID 211 | transaction_id = self.transaction_id 212 | 213 | # Append command/arguments to build list 214 | build_command = [command, '-i %i' % transaction_id] 215 | if args: 216 | build_command.extend(args) 217 | if kwargs: 218 | build_command.extend(['-%s %s' % pair for pair in kwargs.items()]) 219 | 220 | # Remove leading/trailing spaces and build command string 221 | build_command = [part.strip() for part in build_command if part.strip()] 222 | command = ' '.join(build_command) 223 | if expression: 224 | command += ' -- ' + H.base64_encode(expression) 225 | 226 | # Show debug output 227 | debug('[Send command] %s' % command) 228 | 229 | # Send command to debugger engine 230 | try: 231 | self.socket.send(H.data_write(command + '\x00')) 232 | except: 233 | e = sys.exc_info()[1] 234 | raise ProtocolConnectionException(e) 235 | 236 | def listen(self): 237 | """ 238 | Create socket server which listens for connection on configured port. 239 | """ 240 | # Create socket server 241 | server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 242 | 243 | if server: 244 | # Configure socket server 245 | try: 246 | server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 247 | server.settimeout(1) 248 | server.bind((self.host, self.port)) 249 | server.listen(1) 250 | self.listening = True 251 | self.socket = None 252 | except: 253 | e = sys.exc_info()[1] 254 | debug('Failed to create socket: %s' % e) 255 | # Substitute exception with readable (custom) message 256 | if isinstance(e, TypeError) and not H.is_number(self.port): 257 | e = 'Configured port is not a valid integer.' 258 | elif isinstance(e, socket.gaierror) and self.host != '': 259 | e = 'Hostname (%s) is not specified in hosts file or is an IPv6 address.' % self.host 260 | elif hasattr(e, 'errno'): 261 | address_or_port = 'address (%s:%d)' % (self.host, self.port) if self.host != '' else 'port (%d)' % self.port 262 | if e.errno == errno.EADDRINUSE: 263 | e = 'Another application is already listening on configured %s.' % address_or_port 264 | elif e.errno == errno.EADDRNOTAVAIL: 265 | e = 'Configured %s is not accessible.' % address_or_port 266 | raise ProtocolListenException(e) 267 | 268 | # Accept incoming connection on configured port 269 | while self.listening: 270 | try: 271 | self.socket, address = server.accept() 272 | self.listening = False 273 | except socket.timeout: 274 | pass 275 | 276 | # Check if a connection has been made 277 | if self.socket: 278 | self.connected = True 279 | self.socket.settimeout(None) 280 | else: 281 | self.connected = False 282 | self.listening = False 283 | 284 | # Close socket server 285 | try: 286 | server.close() 287 | except: 288 | pass 289 | server = None 290 | 291 | # Return socket connection 292 | return self.socket 293 | else: 294 | raise ProtocolListenException('Could not create socket server.') 295 | 296 | 297 | class ProtocolException(Exception): 298 | pass 299 | 300 | 301 | class ProtocolConnectionException(ProtocolException): 302 | pass 303 | 304 | 305 | class ProtocolListenException(ProtocolException): 306 | pass 307 | -------------------------------------------------------------------------------- /xdebug/session.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | 3 | import sys 4 | import threading 5 | 6 | # Helper module 7 | try: 8 | from .helper import H 9 | except: 10 | from helper import H 11 | 12 | # Settings variables 13 | try: 14 | from . import settings as S 15 | except: 16 | import settings as S 17 | 18 | # DBGp protocol constants 19 | try: 20 | from . import dbgp 21 | except: 22 | import dbgp 23 | 24 | # Config module 25 | from .config import get_value 26 | 27 | # Log module 28 | from .log import debug, info 29 | 30 | # Protocol module 31 | from .protocol import ProtocolConnectionException 32 | 33 | # Util module 34 | from .util import get_real_path 35 | 36 | # View module 37 | from .view import DATA_CONTEXT, DATA_STACK, DATA_WATCH, TITLE_WINDOW_WATCH, generate_context_output, generate_stack_output, get_response_properties, has_debug_view, render_regions, show_content, show_file, show_panel_content 38 | 39 | 40 | ACTION_EVALUATE = 'action_evaluate' 41 | ACTION_EXECUTE = 'action_execute' 42 | ACTION_INIT = 'action_init' 43 | ACTION_REMOVE_BREAKPOINT = 'action_remove_breakpoint' 44 | ACTION_SET_BREAKPOINT = 'action_set_breakpoint' 45 | ACTION_STATUS = 'action_status' 46 | ACTION_USER_EXECUTE = 'action_user_execute' 47 | ACTION_WATCH = 'action_watch' 48 | 49 | 50 | def is_connected(show_status=False): 51 | """ 52 | Check if client is connected to debugger engine. 53 | 54 | Keyword arguments: 55 | show_status -- Show message why client is not connected in status bar. 56 | """ 57 | if S.SESSION and S.SESSION.connected: 58 | return True 59 | elif S.SESSION and show_status: 60 | sublime.status_message('Xdebug: Waiting for response from debugger engine.') 61 | elif show_status: 62 | sublime.status_message('Xdebug: No Xdebug session running.') 63 | return False 64 | 65 | 66 | def connection_error(message): 67 | """ 68 | Template for showing error message on connection error/loss. 69 | 70 | Keyword arguments: 71 | message -- Exception/reason of connection error/loss. 72 | """ 73 | sublime.error_message('Please restart Xdebug debugging session.\nDisconnected from Xdebug debugger engine.\n' + message) 74 | info('Connection lost with debugger engine.') 75 | debug(message) 76 | # Reset connection 77 | try: 78 | S.SESSION.clear() 79 | except: 80 | pass 81 | finally: 82 | S.SESSION = None 83 | S.SESSION_BUSY = False 84 | S.BREAKPOINT_EXCEPTION = None 85 | S.BREAKPOINT_ROW = None 86 | S.BREAKPOINT_RUN = None 87 | S.CONTEXT_DATA.clear() 88 | async_session = SocketHandler(ACTION_WATCH) 89 | async_session.start() 90 | # Reset layout 91 | sublime.active_window().run_command('xdebug_layout') 92 | # Render breakpoint markers 93 | render_regions() 94 | 95 | 96 | class SocketHandler(threading.Thread): 97 | def __init__(self, action, **options): 98 | threading.Thread.__init__(self) 99 | self.action = action 100 | self.options = options 101 | 102 | def get_option(self, option, default_value=None): 103 | if option in self.options.keys(): 104 | return self.options[option] 105 | return default_value 106 | 107 | def run_command(self, command, args=None): 108 | if not isinstance(args, dict): 109 | args = {} 110 | self.timeout(lambda: self._run_command(command, args)) 111 | 112 | def _run_command(self, command, args=None): 113 | try: 114 | sublime.active_window().run_command(command, args) 115 | except: 116 | # In case active_window() is not available 117 | pass 118 | 119 | def run_view_command(self, command, args=None): 120 | if not isinstance(args, dict): 121 | args = {} 122 | self.timeout(lambda: self._run_view_command) 123 | 124 | def _run_view_command(self, command, args=None): 125 | try: 126 | sublime.active_window().active_view().run_command(command, args) 127 | except: 128 | # In case there is no active_view() available 129 | pass 130 | 131 | def status_message(self, message): 132 | sublime.set_timeout(lambda: sublime.status_message(message), 100) 133 | 134 | def timeout(self, function): 135 | sublime.set_timeout(function, 0) 136 | 137 | def run(self): 138 | # Make sure an action is defined 139 | if not self.action: 140 | return 141 | try: 142 | S.SESSION_BUSY = True 143 | # Evaluate 144 | if self.action == ACTION_EVALUATE: 145 | self.evaluate(self.get_option('expression')) 146 | # Execute 147 | elif self.action == ACTION_EXECUTE: 148 | self.execute(self.get_option('command')) 149 | # Init 150 | elif self.action == ACTION_INIT: 151 | self.init() 152 | # Remove breakpoint 153 | elif self.action == ACTION_REMOVE_BREAKPOINT: 154 | self.remove_breakpoint(self.get_option('breakpoint_id')) 155 | # Set breakpoint 156 | elif self.action == ACTION_SET_BREAKPOINT: 157 | self.set_breakpoint(self.get_option('filename'), self.get_option('lineno'), self.get_option('expression')) 158 | # Status 159 | elif self.action == ACTION_STATUS: 160 | self.status() 161 | # User defined execute 162 | elif self.action == ACTION_USER_EXECUTE: 163 | self.user_execute(self.get_option('command'), self.get_option('args')) 164 | # Watch expression 165 | elif self.action == ACTION_WATCH: 166 | self.watch_expression() 167 | # Show dialog on connection error 168 | except ProtocolConnectionException: 169 | e = sys.exc_info()[1] 170 | self.timeout(lambda: connection_error('%s' % e)) 171 | finally: 172 | S.SESSION_BUSY = False 173 | 174 | def evaluate(self, expression): 175 | if not expression or not is_connected(): 176 | return 177 | # Send 'eval' command to debugger engine with code to evaluate 178 | S.SESSION.send(dbgp.EVAL, expression=expression) 179 | if get_value(S.KEY_PRETTY_OUTPUT): 180 | response = S.SESSION.read() 181 | properties = get_response_properties(response, expression) 182 | response = generate_context_output(properties) 183 | else: 184 | response = S.SESSION.read(return_string=True) 185 | 186 | # Show response data in output panel 187 | self.timeout(lambda: show_panel_content(response)) 188 | 189 | def execute(self, command): 190 | # Do not execute if no command is set 191 | if not command or not is_connected(): 192 | return 193 | 194 | # Send command to debugger engine 195 | S.SESSION.send(command) 196 | response = S.SESSION.read() 197 | 198 | # Reset previous breakpoint values 199 | S.BREAKPOINT_EXCEPTION = None 200 | S.BREAKPOINT_ROW = None 201 | S.CONTEXT_DATA.clear() 202 | self.watch_expression() 203 | # Set debug layout 204 | self.run_command('xdebug_layout') 205 | 206 | # Handle breakpoint hit 207 | for child in response: 208 | if child.tag == dbgp.ELEMENT_BREAKPOINT or child.tag == dbgp.ELEMENT_PATH_BREAKPOINT or child.tag == dbgp.ELEMENT_PATH_SECURE_BREAKPOINT: 209 | # Get breakpoint attribute values 210 | fileuri = child.get(dbgp.BREAKPOINT_FILENAME) 211 | lineno = child.get(dbgp.BREAKPOINT_LINENO) 212 | exception = child.get(dbgp.BREAKPOINT_EXCEPTION) 213 | filename = get_real_path(fileuri) 214 | if (exception): 215 | info(exception + ': ' + child.text) 216 | # Remember Exception name and first line of message 217 | S.BREAKPOINT_EXCEPTION = {'name': exception, 'message': child.text.split('\n')[0], 'filename': fileuri, 'lineno': lineno} 218 | 219 | # Check if temporary breakpoint is set and hit 220 | if S.BREAKPOINT_RUN is not None and S.BREAKPOINT_RUN['filename'] == filename and S.BREAKPOINT_RUN['lineno'] == lineno: 221 | # Remove temporary breakpoint 222 | if S.BREAKPOINT_RUN['filename'] in S.BREAKPOINT and S.BREAKPOINT_RUN['lineno'] in S.BREAKPOINT[S.BREAKPOINT_RUN['filename']]: 223 | self.run_view_command('xdebug_breakpoint', {'rows': [S.BREAKPOINT_RUN['lineno']], 'filename': S.BREAKPOINT_RUN['filename']}) 224 | S.BREAKPOINT_RUN = None 225 | # Skip if temporary breakpoint was not hit 226 | if S.BREAKPOINT_RUN is not None and (S.BREAKPOINT_RUN['filename'] != filename or S.BREAKPOINT_RUN['lineno'] != lineno): 227 | self.run_command('xdebug_execute', {'command': 'run'}) 228 | return 229 | # Show debug/status output 230 | self.status_message('Xdebug: Breakpoint') 231 | info('Break: ' + filename + ':' + lineno) 232 | # Store line number of breakpoint for displaying region marker 233 | S.BREAKPOINT_ROW = {'filename': filename, 'lineno': lineno} 234 | # Focus/Open file window view 235 | self.timeout(lambda: show_file(filename, lineno)) 236 | 237 | # On breakpoint get context variables and stack history 238 | if response.get(dbgp.ATTRIBUTE_STATUS) == dbgp.STATUS_BREAK: 239 | # Context variables 240 | context = self.get_context_values() 241 | self.timeout(lambda: show_content(DATA_CONTEXT, context)) 242 | 243 | # Stack history 244 | stack = self.get_stack_values() 245 | self.timeout(lambda: show_content(DATA_STACK, stack)) 246 | 247 | # Watch expressions 248 | self.watch_expression() 249 | 250 | # Reload session when session stopped, by reaching end of file or interruption 251 | if response.get(dbgp.ATTRIBUTE_STATUS) == dbgp.STATUS_STOPPING or response.get(dbgp.ATTRIBUTE_STATUS) == dbgp.STATUS_STOPPED: 252 | self.run_command('xdebug_session_stop', {'restart': True}) 253 | self.run_command('xdebug_session_start', {'restart': True}) 254 | self.status_message('Xdebug: Finished executing file on server. Reload page to continue debugging.') 255 | 256 | # Render breakpoint markers 257 | self.timeout(lambda: render_regions()) 258 | 259 | def get_context_values(self): 260 | """ 261 | Get variables in current context. 262 | """ 263 | if not is_connected(): 264 | return 265 | 266 | context = H.new_dictionary() 267 | try: 268 | # Super global variables 269 | if get_value(S.KEY_SUPER_GLOBALS): 270 | S.SESSION.send(dbgp.CONTEXT_GET, c=dbgp.CONTEXT_ID_SUPERGLOBALS) 271 | response = S.SESSION.read() 272 | context.update(get_response_properties(response)) 273 | 274 | # Local variables 275 | S.SESSION.send(dbgp.CONTEXT_GET) 276 | response = S.SESSION.read() 277 | context.update(get_response_properties(response)) 278 | except ProtocolConnectionException: 279 | e = sys.exc_info()[1] 280 | self.timeout(lambda: connection_error('%s' % e)) 281 | 282 | # Store context variables in session 283 | S.CONTEXT_DATA = context 284 | 285 | return generate_context_output(context) 286 | 287 | def get_stack_values(self): 288 | """ 289 | Get stack information for current context. 290 | """ 291 | response = None 292 | if is_connected(): 293 | try: 294 | # Get stack information 295 | S.SESSION.send(dbgp.STACK_GET) 296 | response = S.SESSION.read() 297 | except ProtocolConnectionException: 298 | e = sys.exc_info()[1] 299 | self.timeout(lambda: connection_error('%s' % e)) 300 | return generate_stack_output(response) 301 | 302 | def get_watch_values(self): 303 | """ 304 | Evaluate all watch expressions in current context. 305 | """ 306 | for index, item in enumerate(S.WATCH): 307 | # Reset value for watch expression 308 | S.WATCH[index]['value'] = None 309 | 310 | # Evaluate watch expression when connected to debugger engine 311 | if is_connected(): 312 | if item['enabled']: 313 | watch_value = None 314 | try: 315 | S.SESSION.send(dbgp.EVAL, expression=item['expression']) 316 | response = S.SESSION.read() 317 | 318 | watch_value = get_response_properties(response, item['expression']) 319 | except ProtocolConnectionException: 320 | pass 321 | 322 | S.WATCH[index]['value'] = watch_value 323 | 324 | def init(self): 325 | if not is_connected(): 326 | return 327 | 328 | # Connection initialization 329 | init = S.SESSION.read() 330 | 331 | # More detailed internal information on properties 332 | S.SESSION.send(dbgp.FEATURE_SET, n='show_hidden', v=1) 333 | S.SESSION.read() 334 | 335 | # Set max children limit 336 | max_children = get_value(S.KEY_MAX_CHILDREN) 337 | if max_children is not False and max_children is not True and (H.is_number(max_children) or H.is_digit(max_children)): 338 | S.SESSION.send(dbgp.FEATURE_SET, n=dbgp.FEATURE_NAME_MAX_CHILDREN, v=max_children) 339 | S.SESSION.read() 340 | 341 | # Set max data limit 342 | max_data = get_value(S.KEY_MAX_DATA) 343 | if max_data is not False and max_data is not True and (H.is_number(max_data) or H.is_digit(max_data)): 344 | S.SESSION.send(dbgp.FEATURE_SET, n=dbgp.FEATURE_NAME_MAX_DATA, v=max_data) 345 | S.SESSION.read() 346 | 347 | # Set max depth limit 348 | max_depth = get_value(S.KEY_MAX_DEPTH) 349 | if max_depth is not False and max_depth is not True and (H.is_number(max_depth) or H.is_digit(max_depth)): 350 | S.SESSION.send(dbgp.FEATURE_SET, n=dbgp.FEATURE_NAME_MAX_DEPTH, v=max_depth) 351 | S.SESSION.read() 352 | 353 | # Set breakpoints for files 354 | for filename, breakpoint_data in S.BREAKPOINT.items(): 355 | if breakpoint_data: 356 | for lineno, bp in breakpoint_data.items(): 357 | if bp['enabled']: 358 | self.set_breakpoint(filename, lineno, bp['expression']) 359 | debug('breakpoint_set: ' + filename + ':' + lineno) 360 | 361 | # Set breakpoints for exceptions 362 | break_on_exception = get_value(S.KEY_BREAK_ON_EXCEPTION) 363 | if isinstance(break_on_exception, list): 364 | for exception_name in break_on_exception: 365 | self.set_exception(exception_name) 366 | 367 | # Determine if client should break at first line on connect 368 | if get_value(S.KEY_BREAK_ON_START): 369 | # Get init attribute values 370 | fileuri = init.get(dbgp.INIT_FILEURI) 371 | filename = get_real_path(fileuri) 372 | # Show debug/status output 373 | self.status_message('Xdebug: Break on start') 374 | info('Break on start: ' + filename) 375 | # Store line number of breakpoint for displaying region marker 376 | S.BREAKPOINT_ROW = {'filename': filename, 'lineno': 1} 377 | # Focus/Open file window view 378 | self.timeout(lambda: show_file(filename, 1)) 379 | 380 | # Context variables 381 | context = self.get_context_values() 382 | self.timeout(lambda: show_content(DATA_CONTEXT, context)) 383 | 384 | # Stack history 385 | stack = self.get_stack_values() 386 | if not stack: 387 | stack = H.unicode_string('[{level}] {filename}.{where}:{lineno}\n' 388 | .format(level=0, where='{main}', lineno=1, filename=fileuri)) 389 | self.timeout(lambda: show_content(DATA_STACK, stack)) 390 | 391 | # Watch expressions 392 | self.watch_expression() 393 | else: 394 | # Tell script to run it's process 395 | self.run_command('xdebug_execute', {'command': 'run'}) 396 | 397 | def remove_breakpoint(self, breakpoint_id): 398 | if not breakpoint_id or not is_connected(): 399 | return 400 | 401 | S.SESSION.send(dbgp.BREAKPOINT_REMOVE, d=breakpoint_id) 402 | S.SESSION.read() 403 | 404 | def set_breakpoint(self, filename, lineno, expression=None): 405 | if not filename or not lineno or not is_connected(): 406 | return 407 | 408 | # Get path of file on server 409 | fileuri = get_real_path(filename, True) 410 | # Set breakpoint 411 | S.SESSION.send(dbgp.BREAKPOINT_SET, t='line', f=fileuri, n=lineno, expression=expression) 412 | response = S.SESSION.read() 413 | # Update breakpoint id 414 | breakpoint_id = response.get(dbgp.ATTRIBUTE_BREAKPOINT_ID) 415 | if breakpoint_id: 416 | S.BREAKPOINT[filename][lineno]['id'] = breakpoint_id 417 | 418 | def set_exception(self, exception): 419 | if not is_connected(): 420 | return 421 | 422 | S.SESSION.send(dbgp.BREAKPOINT_SET, t='exception', x='"%s"' % exception) 423 | S.SESSION.read() 424 | 425 | def status(self): 426 | if not is_connected(): 427 | return 428 | 429 | # Send 'status' command to debugger engine 430 | S.SESSION.send(dbgp.STATUS) 431 | response = S.SESSION.read() 432 | # Show response in status bar 433 | self.status_message('Xdebug status: ' + response.get(dbgp.ATTRIBUTE_REASON) + ' - ' + response.get(dbgp.ATTRIBUTE_STATUS)) 434 | 435 | def user_execute(self, command, args=None): 436 | if not command or not is_connected(): 437 | return 438 | 439 | # Send command to debugger engine 440 | S.SESSION.send(command, args) 441 | response = S.SESSION.read(return_string=True) 442 | 443 | # Show response data in output panel 444 | self.timeout(lambda: show_panel_content(response)) 445 | 446 | def watch_expression(self): 447 | # Evaluate watch expressions 448 | self.get_watch_values() 449 | # Show watch expression 450 | self.timeout(lambda: self._watch_expression(self.get_option('check_watch_view', False))) 451 | 452 | def _watch_expression(self, check_watch_view): 453 | # Do not show if we only want to show content when Watch view is not available 454 | if check_watch_view and not has_debug_view(TITLE_WINDOW_WATCH): 455 | return 456 | 457 | show_content(DATA_WATCH) 458 | -------------------------------------------------------------------------------- /xdebug/settings.py: -------------------------------------------------------------------------------- 1 | DEFAULT_HOST = '' 2 | DEFAULT_PORT = 9000 3 | DEFAULT_IDE_KEY = 'sublime.xdebug' 4 | 5 | PACKAGE_PATH = None 6 | PACKAGE_FOLDER = None 7 | 8 | FILE_LOG_OUTPUT = 'Xdebug.log' 9 | FILE_BREAKPOINT_DATA = 'Xdebug.breakpoints' 10 | FILE_PACKAGE_SETTINGS = 'Xdebug.sublime-settings' 11 | FILE_WATCH_DATA = 'Xdebug.expressions' 12 | 13 | KEY_SETTINGS = 'settings' 14 | KEY_XDEBUG = 'xdebug' 15 | 16 | KEY_PATH_MAPPING = 'path_mapping' 17 | KEY_URL = 'url' 18 | KEY_IDE_KEY = 'ide_key' 19 | KEY_HOST = 'host' 20 | KEY_PORT = 'port' 21 | KEY_MAX_CHILDREN = 'max_children' 22 | KEY_MAX_DATA = 'max_data' 23 | KEY_MAX_DEPTH = 'max_depth' 24 | KEY_BREAK_ON_START = 'break_on_start' 25 | KEY_BREAK_ON_EXCEPTION = 'break_on_exception' 26 | KEY_CLOSE_ON_STOP = 'close_on_stop' 27 | KEY_SUPER_GLOBALS = 'super_globals' 28 | KEY_FULLNAME_PROPERTY = 'fullname_property' 29 | KEY_HIDE_PASSWORD = 'hide_password' 30 | KEY_PRETTY_OUTPUT = 'pretty_output' 31 | KEY_LAUNCH_BROWSER = 'launch_browser' 32 | KEY_BROWSER_NO_EXECUTE = 'browser_no_execute' 33 | KEY_DISABLE_LAYOUT = 'disable_layout' 34 | KEY_DEBUG_LAYOUT = 'debug_layout' 35 | 36 | KEY_BREAKPOINT_GROUP = 'breakpoint_group' 37 | KEY_BREAKPOINT_INDEX = 'breakpoint_index' 38 | KEY_CONTEXT_GROUP = 'context_group' 39 | KEY_CONTEXT_INDEX = 'context_index' 40 | KEY_STACK_GROUP = 'stack_group' 41 | KEY_STACK_INDEX = 'stack_index' 42 | KEY_WATCH_GROUP = 'watch_group' 43 | KEY_WATCH_INDEX = 'watch_index' 44 | 45 | KEY_BREAKPOINT_CURRENT = 'breakpoint_current' 46 | KEY_BREAKPOINT_DISABLED = 'breakpoint_disabled' 47 | KEY_BREAKPOINT_ENABLED = 'breakpoint_enabled' 48 | KEY_CURRENT_LINE = 'current_line' 49 | 50 | KEY_PYTHON_PATH = 'python_path' 51 | KEY_DEBUG = 'debug' 52 | 53 | # Region scope sources 54 | REGION_KEY_BREAKPOINT = 'xdebug_breakpoint' 55 | REGION_KEY_CURRENT = 'xdebug_current' 56 | REGION_KEY_DISABLED = 'xdebug_disabled' 57 | REGION_SCOPE_BREAKPOINT = 'comment.line.xdebug.gutter.breakpoint' 58 | REGION_SCOPE_CURRENT = 'string.quoted.xdebug.gutter.current' 59 | 60 | # Window layout for debugging output 61 | LAYOUT_DEBUG = { 62 | 'cols': [0.0, 0.5, 1.0], 63 | 'rows': [0.0, 0.7, 1.0], 64 | 'cells': [ 65 | [0, 0, 2, 1], 66 | [0, 1, 1, 2], 67 | [1, 1, 2, 2] 68 | ] 69 | } 70 | # Default single layout (similar to Alt+Shift+1) 71 | LAYOUT_NORMAL = { 72 | 'cols': [0.0, 1.0], 73 | 'rows': [0.0, 1.0], 74 | 'cells': [ 75 | [0, 0, 1, 1] 76 | ] 77 | } 78 | 79 | RESTORE_LAYOUT = None 80 | RESTORE_INDEX = None 81 | 82 | SESSION_BUSY = False 83 | 84 | SESSION = None 85 | BREAKPOINT = {} 86 | CONTEXT_DATA = {} 87 | WATCH = [] 88 | 89 | BREAKPOINT_EXCEPTION = None 90 | # Breakpoint line number in script being debugged 91 | BREAKPOINT_ROW = None 92 | # Placeholder for temporary breakpoint filename and line number 93 | BREAKPOINT_RUN = None 94 | # Will hold breakpoint line number to show for file which is being loaded 95 | SHOW_ROW_ONLOAD = {} 96 | 97 | CONFIG_PROJECT = None 98 | CONFIG_PACKAGE = None 99 | CONFIG_KEYS = [ 100 | KEY_PATH_MAPPING, 101 | KEY_URL, 102 | KEY_IDE_KEY, 103 | KEY_HOST, 104 | KEY_PORT, 105 | KEY_MAX_CHILDREN, 106 | KEY_MAX_DATA, 107 | KEY_MAX_DEPTH, 108 | KEY_BREAK_ON_START, 109 | KEY_BREAK_ON_EXCEPTION, 110 | KEY_CLOSE_ON_STOP, 111 | KEY_SUPER_GLOBALS, 112 | KEY_FULLNAME_PROPERTY, 113 | KEY_HIDE_PASSWORD, 114 | KEY_PRETTY_OUTPUT, 115 | KEY_LAUNCH_BROWSER, 116 | KEY_BROWSER_NO_EXECUTE, 117 | KEY_DISABLE_LAYOUT, 118 | KEY_DEBUG_LAYOUT, 119 | KEY_BREAKPOINT_GROUP, 120 | KEY_BREAKPOINT_INDEX, 121 | KEY_CONTEXT_GROUP, 122 | KEY_CONTEXT_INDEX, 123 | KEY_STACK_GROUP, 124 | KEY_STACK_INDEX, 125 | KEY_WATCH_GROUP, 126 | KEY_WATCH_INDEX, 127 | KEY_BREAKPOINT_CURRENT, 128 | KEY_BREAKPOINT_DISABLED, 129 | KEY_BREAKPOINT_ENABLED, 130 | KEY_CURRENT_LINE, 131 | KEY_PYTHON_PATH, 132 | KEY_DEBUG 133 | ] 134 | -------------------------------------------------------------------------------- /xdebug/unittesting.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sublime 3 | import threading 4 | from urllib import request 5 | from urllib.parse import urlencode 6 | from unittesting import DeferrableTestCase 7 | 8 | 9 | class XdebugDeferrableTestCase(DeferrableTestCase): 10 | # Using realpath in order to resolve any symbolic links otherwise path mapping will not work 11 | package_path = os.environ.get('TRAVIS_BUILD_DIR', os.path.realpath(os.path.join(sublime.packages_path(), 'SublimeTextXdebug'))) 12 | platform = os.environ.get('TRAVIS_OS_NAME', 'docker') 13 | 14 | local_path = os.path.join(package_path, 'tests', 'server', 'public') 15 | remote_path = local_path if platform == 'osx' else '/var/www/html' 16 | server_host = '127.0.0.1:8090' 17 | 18 | project_data = { 19 | 'folders': [ 20 | {'path': local_path} 21 | ], 22 | 'settings': { 23 | # Ensure Sublime Text window remains opened at all times 24 | 'close_windows_when_empty': False, 25 | # Force tabs in order to properly assert content 26 | 'translate_tabs_to_spaces': False, 27 | # Specify default settings in case of running tests locally, 28 | # to prevent failure with defined (conflicting) user settings 29 | 'xdebug': { 30 | 'path_mapping': {}, 31 | 'url': '', 32 | 'ide_key': 'sublime.xdebug', 33 | 'host': '', 34 | 'port': 9000, 35 | 'max_children': 32, 36 | 'max_data': 1024, 37 | 'max_depth': 1, 38 | 'break_on_start': False, 39 | 'break_on_exception': [ 40 | 'Fatal error', 41 | 'Catchable fatal error', 42 | 'Warning', 43 | 'Parse error', 44 | 'Notice', 45 | 'Strict standards', 46 | 'Deprecated', 47 | 'Xdebug', 48 | 'Unknown error' 49 | ], 50 | 'close_on_stop': False, 51 | 'super_globals': True, 52 | 'fullname_property': True, 53 | 'hide_password': False, 54 | 'pretty_output': False, 55 | 'launch_browser': False, 56 | 'browser_no_execute': False, 57 | 'disable_layout': False, 58 | 'debug_layout': { 59 | 'cols': [0.0, 0.5, 1.0], 60 | 'rows': [0.0, 0.7, 1.0], 61 | 'cells': [[0, 0, 2, 1], [0, 1, 1, 2], [1, 1, 2, 2]] 62 | }, 63 | 'breakpoint_group': 2, 64 | 'breakpoint_index': 1, 65 | 'context_group': 1, 66 | 'context_index': 0, 67 | 'stack_group': 2, 68 | 'stack_index': 0, 69 | 'watch_group': 1, 70 | 'watch_index': 1, 71 | 'breakpoint_enabled': 'circle', 72 | 'breakpoint_disabled': 'dot', 73 | 'breakpoint_current': '', 74 | 'current_line': 'bookmark', 75 | 'python_path': '', 76 | 'debug': False 77 | } 78 | } 79 | } 80 | 81 | # Path mapping is only required when server is on a remote machine. 82 | # Most cases test server is started in Docker (using php-server), 83 | # with the exception of macOS on Travis CI and will be running locally. 84 | if remote_path != local_path: 85 | project_data['settings']['xdebug']['path_mapping'] = { 86 | remote_path: local_path 87 | } 88 | 89 | def setUp(self): 90 | self._xdebug_settings = {} 91 | 92 | project_data = self.project_data.copy() 93 | project_data['settings']['xdebug']['_testMethodName'] = self._testMethodName 94 | sublime.active_window().set_project_data(project_data) 95 | 96 | def has_loaded_project_data(): 97 | return self.get_xdebug_setting('_testMethodName') == self._testMethodName 98 | yield has_loaded_project_data 99 | 100 | def tearDown(self): 101 | # Stop active session to prevent multiple test cases listening to port 9000 102 | self.run_command('xdebug_session_stop') 103 | # Remove any breakpoints to ensure a clean slate 104 | self.run_command('xdebug_clear_all_breakpoints') 105 | # Restore layout and close any files opened during session 106 | self.run_command('xdebug_layout', {'restore': True}) 107 | self.run_command('close_all') 108 | 109 | def assertViewContains(self, view, content): 110 | if not self.view_contains_content(view, content): 111 | title = 'View' 112 | if isinstance(view, sublime.View): 113 | title = view.name() if view.name() else view.file_name() 114 | self.fail(title + ' does not contain "' + content + '".') 115 | 116 | def assertViewIsEmpty(self, view): 117 | if not self.view_is_empty(view): 118 | title = 'View' 119 | if isinstance(view, sublime.View): 120 | title = view.name() if view.name() else view.file_name() 121 | self.fail(title + ' is not empty.') 122 | 123 | def get_contents_of_view(self, view): 124 | if view: 125 | return view.substr(sublime.Region(0, view.size())) 126 | return '' 127 | 128 | def get_view_by_title(self, title): 129 | for view in sublime.active_window().views(): 130 | if view.name() == title or view.file_name() == title: 131 | return view 132 | return None 133 | 134 | def get_xdebug_setting(self, key): 135 | settings = sublime.active_window().active_view().settings().get('xdebug') 136 | if isinstance(settings, dict) and key in settings: 137 | return settings[key] 138 | return None 139 | 140 | def run_command(self, *args, **kwargs): 141 | sublime.active_window().run_command(*args, **kwargs) 142 | 143 | def send_server_request(self, path='', params=''): 144 | if isinstance(params, dict): 145 | params = urlencode(params) 146 | query_string = '?' + params if len(params) else '' 147 | url = 'http://{host}/{path}{query_string}'.format(host=self.server_host, path=path, query_string=query_string) 148 | # Send request to server in separate thread to prevent blocking of test execution 149 | threading.Thread(target=request.urlopen, args=(url,)).start() 150 | print('Request send to {url}'.format(url=url)) 151 | 152 | def set_breakpoint(self, filename, lineno, enabled=True): 153 | self.run_command('xdebug_breakpoint', {'enabled': enabled, 'filename': filename, 'rows': [str(lineno)]}) 154 | 155 | def set_xdebug_settings(self, settings): 156 | project_data = sublime.active_window().project_data() 157 | for key, value in settings.items(): 158 | if value is not None: 159 | project_data['settings']['xdebug'][key] = value 160 | elif key in project_data['settings']['xdebug'].keys(): 161 | del project_data['settings']['xdebug'][key] 162 | # Remember any user defined settings for Xdebug plugin, 163 | # which are to be validated in 'window_has_xdebug_settings' 164 | self._xdebug_settings[key] = value 165 | sublime.active_window().set_project_data(project_data) 166 | 167 | def view_contains_content(self, view, content): 168 | view_contents = self.get_contents_of_view(view) 169 | return content in view_contents 170 | 171 | def view_is_empty(self, view): 172 | view_contents = self.get_contents_of_view(view) 173 | return not view_contents 174 | 175 | def window_has_debug_layout(self): 176 | for view in sublime.active_window().views(): 177 | # Watch view is last to initialize 178 | if view.name() == 'Xdebug Watch': 179 | return True 180 | return False 181 | 182 | def window_has_xdebug_settings(self): 183 | for key, value in self._xdebug_settings.items(): 184 | if self.get_xdebug_setting(key) != value: 185 | return False 186 | return True 187 | -------------------------------------------------------------------------------- /xdebug/util.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | 3 | import json 4 | import os 5 | import re 6 | import sys 7 | import webbrowser 8 | 9 | # Helper module 10 | try: 11 | from .helper import H 12 | except: 13 | from helper import H 14 | 15 | # Settings variables 16 | try: 17 | from . import settings as S 18 | except: 19 | import settings as S 20 | 21 | # Config module 22 | from .config import get_value 23 | 24 | # Log module 25 | from .log import debug, info 26 | 27 | 28 | def get_real_path(uri, server=False): 29 | """ 30 | Get real path 31 | 32 | Keyword arguments: 33 | uri -- Uri of file that needs to be mapped and located 34 | server -- Map local path to server path 35 | 36 | TODO: Fix mapping for root (/) and drive letters (P:/) 37 | """ 38 | if uri is None: 39 | return uri 40 | 41 | # URLdecode uri 42 | uri = H.url_decode(uri) 43 | 44 | # Split scheme from uri to get absolute path 45 | try: 46 | # scheme:///path/file => scheme, /path/file 47 | # scheme:///C:/path/file => scheme, C:/path/file 48 | transport, filename = uri.split(':///', 1) 49 | except: 50 | filename = uri 51 | 52 | # Normalize path for comparison and remove duplicate/trailing slashes 53 | uri = os.path.normpath(filename) 54 | 55 | # Pattern for checking if uri is a windows path 56 | drive_pattern = re.compile(r'^[a-zA-Z]:[\\/]') 57 | 58 | # Append leading slash if filesystem is not Windows 59 | if not drive_pattern.match(uri) and not os.path.isabs(uri): 60 | uri = os.path.normpath('/' + uri) 61 | 62 | path_mapping = get_value(S.KEY_PATH_MAPPING) 63 | if isinstance(path_mapping, dict): 64 | # Go through path mappings 65 | for server_path, local_path in path_mapping.items(): 66 | server_path = os.path.normpath(server_path) 67 | local_path = os.path.normpath(local_path) 68 | # Replace path if mapping available 69 | if server: 70 | # Map local path to server path 71 | if local_path in uri: 72 | uri = uri.replace(local_path, server_path) 73 | break 74 | else: 75 | # Map server path to local path 76 | if server_path in uri: 77 | uri = uri.replace(server_path, local_path) 78 | break 79 | else: 80 | sublime.set_timeout(lambda: sublime.status_message('Xdebug: No path mapping defined, returning given path.'), 100) 81 | 82 | # Replace slashes 83 | if not drive_pattern.match(uri): 84 | uri = uri.replace('\\', '/') 85 | 86 | # Append scheme 87 | if server: 88 | return H.url_encode('file://' + uri) 89 | 90 | return uri 91 | 92 | 93 | def get_region_icon(icon): 94 | # Default icons for color schemes from default theme 95 | default_current = 'bookmark' 96 | default_disabled = 'dot' 97 | default_enabled = 'circle' 98 | 99 | # Package icons (without .png extension) 100 | package_breakpoint_current = 'breakpoint_current' 101 | package_breakpoint_disabled = 'breakpoint_disabled' 102 | package_breakpoint_enabled = 'breakpoint_enabled' 103 | package_current_line = 'current_line' 104 | 105 | # List to check for duplicate icon entries 106 | icon_list = [default_current, default_disabled, default_enabled] 107 | 108 | # Determine icon path 109 | icon_path = None 110 | if S.PACKAGE_FOLDER is not None: 111 | # Strip .sublime-package of package name for comparison 112 | package_extension = '.sublime-package' 113 | current_package = S.PACKAGE_FOLDER 114 | if current_package.endswith(package_extension): 115 | current_package = current_package[:-len(package_extension)] 116 | if sublime.version() == '' or int(sublime.version()) > 3000: 117 | # ST3: Packages/Xdebug Client/icons/breakpoint_enabled.png 118 | icon_path = 'Packages/' + current_package + '/icons/{0}.png' 119 | else: 120 | # ST2: ../Xdebug Client/icons/breakpoint_enabled 121 | icon_path = '../' + current_package + '/icons/{0}' 122 | # Append icon path to package icons 123 | package_breakpoint_current = icon_path.format(package_breakpoint_current) 124 | package_breakpoint_disabled = icon_path.format(package_breakpoint_disabled) 125 | package_breakpoint_enabled = icon_path.format(package_breakpoint_enabled) 126 | package_current_line = icon_path.format(package_current_line) 127 | # Add to duplicate list 128 | icon_list.append(icon_path.format(package_breakpoint_current)) 129 | icon_list.append(icon_path.format(package_breakpoint_disabled)) 130 | icon_list.append(icon_path.format(package_breakpoint_enabled)) 131 | icon_list.append(icon_path.format(package_current_line)) 132 | 133 | # Get user defined icons from settings 134 | breakpoint_current = get_value(S.KEY_BREAKPOINT_CURRENT) 135 | breakpoint_disabled = get_value(S.KEY_BREAKPOINT_DISABLED) 136 | breakpoint_enabled = get_value(S.KEY_BREAKPOINT_ENABLED) 137 | current_line = get_value(S.KEY_CURRENT_LINE) 138 | 139 | # Duplicate check, enabled breakpoint 140 | if breakpoint_enabled not in icon_list: 141 | icon_list.append(breakpoint_enabled) 142 | else: 143 | breakpoint_enabled = None 144 | # Duplicate check, disabled breakpoint 145 | if breakpoint_disabled not in icon_list: 146 | icon_list.append(breakpoint_disabled) 147 | else: 148 | breakpoint_disabled = None 149 | # Duplicate check, current line 150 | if current_line not in icon_list: 151 | icon_list.append(current_line) 152 | else: 153 | current_line = None 154 | # Duplicate check, current breakpoint 155 | if breakpoint_current not in icon_list: 156 | icon_list.append(breakpoint_current) 157 | else: 158 | breakpoint_current = None 159 | 160 | # Use default/package icon if no user defined or duplicate detected 161 | if not breakpoint_current and icon_path is not None: 162 | breakpoint_current = package_breakpoint_current 163 | if not breakpoint_disabled: 164 | breakpoint_disabled = default_disabled if icon_path is None else package_breakpoint_disabled 165 | if not breakpoint_enabled: 166 | breakpoint_enabled = default_enabled if icon_path is None else package_breakpoint_enabled 167 | if not current_line: 168 | current_line = default_current if icon_path is None else package_current_line 169 | 170 | # Return icon for icon name 171 | if icon == S.KEY_CURRENT_LINE: 172 | return current_line 173 | elif icon == S.KEY_BREAKPOINT_CURRENT: 174 | return breakpoint_current 175 | elif icon == S.KEY_BREAKPOINT_DISABLED: 176 | return breakpoint_disabled 177 | elif icon == S.KEY_BREAKPOINT_ENABLED: 178 | return breakpoint_enabled 179 | else: 180 | info('Invalid icon name. (' + icon + ')') 181 | return 182 | 183 | 184 | def launch_browser(): 185 | url = get_value(S.KEY_URL) 186 | if not url: 187 | sublime.set_timeout(lambda: sublime.status_message('Xdebug: No URL defined in (project) settings file.'), 100) 188 | return 189 | ide_key = get_value(S.KEY_IDE_KEY, S.DEFAULT_IDE_KEY) 190 | operator = '?' 191 | 192 | # Check if url already has query string 193 | if url.count('?'): 194 | operator = '&' 195 | 196 | # Start debug session 197 | if S.SESSION and (S.SESSION.listening or not S.SESSION.connected): 198 | webbrowser.open(url + operator + 'XDEBUG_SESSION_START=' + ide_key) 199 | # Stop debug session 200 | else: 201 | # Check if we should execute script 202 | if get_value(S.KEY_BROWSER_NO_EXECUTE): 203 | # Without executing script 204 | webbrowser.open(url + operator + 'XDEBUG_SESSION_STOP_NO_EXEC=' + ide_key) 205 | else: 206 | # Run script normally 207 | webbrowser.open(url + operator + 'XDEBUG_SESSION_STOP=' + ide_key) 208 | 209 | 210 | def load_breakpoint_data(): 211 | data_path = os.path.join(sublime.packages_path(), 'User', S.FILE_BREAKPOINT_DATA) 212 | data = {} 213 | 214 | if os.path.isfile(data_path): 215 | try: 216 | data_file = open(data_path, 'rb') 217 | data = json.loads(H.data_read(data_file.read())) 218 | except: 219 | info('Failed to open/parse %s.' % data_path) 220 | debug(sys.exc_info()[1]) 221 | 222 | # Do not use deleted files or entries without breakpoints 223 | if data: 224 | for filename, breakpoint_data in data.copy().items(): 225 | if not breakpoint_data or not os.path.isfile(filename): 226 | del data[filename] 227 | 228 | if not isinstance(S.BREAKPOINT, dict): 229 | S.BREAKPOINT = {} 230 | 231 | # Set breakpoint data 232 | S.BREAKPOINT.update(data) 233 | 234 | 235 | def load_watch_data(): 236 | data_path = os.path.join(sublime.packages_path(), 'User', S.FILE_WATCH_DATA) 237 | data = [] 238 | 239 | if os.path.isfile(data_path): 240 | try: 241 | data_file = open(data_path, 'rb') 242 | data = json.loads(H.data_read(data_file.read())) 243 | except: 244 | info('Failed to open/parse %s.' % data_path) 245 | debug(sys.exc_info()[1]) 246 | 247 | # Check if expression is not already defined 248 | duplicates = [] 249 | for index, entry in enumerate(data): 250 | matches = [x for x in S.WATCH if x['expression'] == entry['expression']] 251 | if matches: 252 | duplicates.append(entry) 253 | else: 254 | # Unset any previous value 255 | data[index]['value'] = None 256 | for duplicate in duplicates: 257 | data.remove(duplicate) 258 | 259 | if not isinstance(S.WATCH, list): 260 | S.WATCH = [] 261 | 262 | # Set watch data 263 | S.WATCH.extend(data) 264 | 265 | 266 | def save_breakpoint_data(): 267 | data_path = os.path.join(sublime.packages_path(), 'User', S.FILE_BREAKPOINT_DATA) 268 | with open(data_path, 'wb') as data: 269 | data.write(H.data_write(json.dumps(S.BREAKPOINT))) 270 | 271 | 272 | def save_watch_data(): 273 | data_path = os.path.join(sublime.packages_path(), 'User', S.FILE_WATCH_DATA) 274 | with open(data_path, 'wb') as data: 275 | data.write(H.data_write(json.dumps(S.WATCH))) 276 | --------------------------------------------------------------------------------