evandrocoan / AmxxEditor

Auto-complete and Build System for Amx Mod X on Sublime Text
GNU General Public License v3.0
9 stars 3 forks source link

UnicodeDecodeError: 'charmap' codec can't decode byte 0x8f in position 256: character maps to <undefined> #21

Open evandrocoan opened 2 years ago

evandrocoan commented 2 years ago
Traceback (most recent call last):
  File "E:\Programs\Sublime Text 3\sublime_plugin.py", line 610, in on_selection_modified_async
    callback.on_selection_modified_async(v)
  File "C:\Users\OciXCrom\AppData\Roaming\Sublime Text 3\Packages\AmxxEditor\AmxxEditor.py", line 280, in on_selection_modified_async
    self.inteltip_function(view, region)
  File "C:\Users\OciXCrom\AppData\Roaming\Sublime Text 3\Packages\AmxxEditor\AmxxEditor.py", line 331, in inteltip_function
    node        = nodes[file_name]
KeyError: 'E:\\Servers\\iPlay.bg Jailbreak\\cstrike\\addons\\amxmodx\\scripting\\jb_extreme.sma'

        try:
            if "include_path.pawn" in scope :
                self.inteltip_include(view, region)
            else :
                self.inteltip_function(view, region)
        except Exception as error:
            print("on_selection_modified_async exception: %s" % error)

Exception in thread Thread-2:
Traceback (most recent call last):
  File "./python3.3/threading.py", line 901, in _bootstrap_inner
  File "C:\Users\OciXCrom\AppData\Roaming\Sublime Text 3\Packages\AmxxEditor\AmxxEditor.py", line 820, in run
    self.process(file_name, view_buffer)
  File "C:\Users\OciXCrom\AppData\Roaming\Sublime Text 3\Packages\AmxxEditor\AmxxEditor.py", line 831, in process
    self.load_from_file(view_file_name, include, current_node, current_node, base_includes)
  File "C:\Users\OciXCrom\AppData\Roaming\Sublime Text 3\Packages\AmxxEditor\AmxxEditor.py", line 878, in load_from_file
    includes = includes_re.findall(f.read())
  File "./python3.3/encodings/cp1252.py", line 23, in decode
UnicodeDecodeError: 'charmap' codec can't decode byte 0x8f in position 256: character maps to <undefined>

Older code used/fixed:

```python #!/usr/bin/env python3 # -*- coding: UTF-8 -*- # # Licensing # # Copyright (C) 2013-2016 ppalex7 # Copyright (C) 2016-2017 AMXX-Editor by Destro # Copyright (C) 2017-2018 Evandro Coan # # Redistributions of source code must retain the above # copyright notice, this list of conditions and the # following disclaimer. # # Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials # provided with the distribution. # # Neither the name Evandro Coan nor the names of any # contributors may be used to endorse or promote products # derived from this software without specific prior written # permission. # # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation; either version 3 of the License, or ( at # your option ) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import re import string import sys import sublime, sublime_plugin import webbrowser import datetime import time import urllib.request from collections import defaultdict, OrderedDict from queue import * from threading import Timer, Thread sys.path.append(os.path.dirname(__file__)) from enum34 import Enum import watchdog.events import watchdog.observers import watchdog.utils from watchdog.utils.bricks import OrderedSetQueue from os.path import basename import logging # Enable editor debug messages: (bitwise) # # 0 - Disabled debugging. # 1 - Errors messages. # 2 - Outputs when it starts a file parsing. # 4 - General messages. # 8 - Analyzer parser. # 16 - Autocomplete debugging. # 32 - Function parsing debugging. # 63 - All debugging levels at the same time. from debug_tools import getLogger log = getLogger( 1, __name__ ) # log = getLogger( 1, __name__, file="amxxeditor.txt", mode='w' ) EDITOR_VERSION = "3.0_zz" CURRENT_PACKAGE_NAME = __package__ g_is_package_loading = True class FUNC_TYPES(Enum): function = 0 public = 1 stock = 2 forward = 3 native = 4 # print( FUNC_TYPES(0) ) g_constants_list = set() g_inteltip_style = "" g_enable_inteltip = False g_enable_buildversion = False g_delay_time = 1.0 g_include_dir = set() g_add_paremeters = False g_new_file_syntax = "Packages/%s/%sPawn.sublime-syntax" % (CURRENT_PACKAGE_NAME, CURRENT_PACKAGE_NAME) g_word_autocomplete = False g_function_autocomplete = False processingSetQueue = OrderedSetQueue() processingSetQueueSet = set() nodes = dict() file_observer = watchdog.observers.Observer() includes_re = re.compile('^[\\s]*#include[\\s]+[<"]([^>"]+)[>"]', re.MULTILINE) local_re = re.compile('\\.(sma|inc)$') function_re = re.compile(r'^[\w_\d: ]*[\w_\d]\(') def plugin_unloaded(): global g_is_package_loading g_is_package_loading=True settings = sublime.load_settings("%s.sublime-settings" % CURRENT_PACKAGE_NAME) settings.clear_on_change(CURRENT_PACKAGE_NAME) log.delete() def plugin_loaded(): settings = sublime.load_settings("%s.sublime-settings" % CURRENT_PACKAGE_NAME) install_build_systens("AmxxEditor.sh") install_build_systens("AmxxEditor.bat") install_setting_file("%s.sublime-settings" % CURRENT_PACKAGE_NAME) install_setting_file("AmxxEditorConsole.sublime-settings") # Fixes the settings dialog showing up when installing the package for the first time global g_is_package_loading g_is_package_loading=True sublime.set_timeout( unlock_is_package_loading, 10000 ) on_settings_modified(); settings.add_on_change(CURRENT_PACKAGE_NAME, on_settings_modified) def unlock_is_package_loading(): global g_is_package_loading g_is_package_loading = False def install_build_systens(target_file_name): target_folder = CURRENT_PACKAGE_NAME target_file = os.path.join( sublime.packages_path(), "User", target_folder, target_file_name ) input_file_string = sublime.load_resource( "Packages/%s/%s" % ( CURRENT_PACKAGE_NAME, target_file_name ) ) target_directory = os.path.join( sublime.packages_path(), "User", target_folder ) attempt_to_install_file( target_directory, target_file, input_file_string ) def install_setting_file( target_file_name ): target_file = os.path.join( sublime.packages_path(), "User", target_file_name ) input_file_string = sublime.load_resource( "Packages/%s/%s" % ( CURRENT_PACKAGE_NAME, target_file_name ) ) target_directory = os.path.join( sublime.packages_path(), "User" ) attempt_to_install_file( target_directory, target_file, input_file_string ) def attempt_to_install_file( target_directory, target_file, input_file_string ): if not os.path.exists( target_directory ): os.makedirs( target_directory ) # How can I force Python's file.write() to use the same newline format in Windows as in Linux (“\r\n” vs. “\n”)? # https://stackoverflow.com/questions/9184107/how-can-i-force-pythons-file-write-to-use-the-same-newline-format-in-windows # # TypeError: 'str' does not support the buffer interface # https://stackoverflow.com/questions/5471158/typeerror-str-does-not-support-the-buffer-interface if not os.path.exists( target_file ): text_file = open( target_file, "wb", errors="ignore" ) text_file.write( bytes(input_file_string, 'UTF-8') ) text_file.close() def unload_handler() : file_observer.stop() process_thread.stop() processingSetQueue.put(("", "")) sublime.load_settings("%s.sublime-settings" % CURRENT_PACKAGE_NAME).clear_on_change(CURRENT_PACKAGE_NAME) class NewAmxxIncludeCommand(sublime_plugin.WindowCommand): def run(self): new_file("inc") class NewAmxxPluginCommand(sublime_plugin.WindowCommand): def run(self): new_file("sma") def new_file(file_type): view = sublime.active_window().new_file() view.set_name("untitled."+file_type) plugin_template = sublime.load_resource("Packages/%s/default.%s" % (CURRENT_PACKAGE_NAME, file_type)) plugin_template = plugin_template.replace("\r", "") view.run_command("insert_snippet", {"contents": plugin_template}) sublime.set_timeout_async( lambda: set_new_file_syntax( view ), 0 ) def set_new_file_syntax( view ): view.set_syntax_file(g_new_file_syntax) class AboutAmxxEditorCommand(sublime_plugin.WindowCommand): def run(self): about = "Sublime AmxxEditor v"+ EDITOR_VERSION +" by Destro\n\n\n" about += "CREDITs:\n" about += "- Great:\n" about += " ppalex7 (SourcePawn Completions)\n\n" about += "- Contributors:\n" about += " sasske (white color scheme)\n" about += " addons_zz (npp color scheme)\n" about += " KliPPy (build version)\n" about += " Mistrick (mistrick color scheme)\n" about += "\nhttps://amxmodx-es.com/showthread.php?tid=12316\n" sublime.message_dialog(about) class AmxxBuildVerCommand(sublime_plugin.TextCommand): def run(self, edit): region = self.view.find("^#define\s+(?:PLUGIN_)?VERSION\s+\".+\"", 0, sublime.IGNORECASE) if region == None : region = self.view.find("new\s+const\s+(?:PLUGIN_)?VERSION\s*\[\s*\]\s*=\s*\".+\"", 0, sublime.IGNORECASE) if region == None : return line = self.view.substr(region) result = re.match("(.*\"(?:v)?\d{1,2}\.\d{1,2}\.(?:\d{1,2}-)?)(\d+)(b(?:eta)?)?\"", line) if not result : return build = int(result.group(2)) build += 1 beta = result.group(3) if not beta : beta = "" self.view.replace(edit, region, result.group(1) + str(build) + beta + '\"') class AmxxEditor(sublime_plugin.EventListener): def __init__(self) : process_thread.start() self.delay_queue = None file_observer.start() def on_window_command(self, window, cmd, args) : if cmd != "build" : return view = window.active_view() if not is_amxmodx_file(view) or not g_enable_buildversion : return view.run_command("amxx_build_ver") def on_selection_modified_async(self, view) : if not is_amxmodx_file(view) or not g_enable_inteltip : return region = view.sel()[0] scope = view.scope_name(region.begin()) log(4, "(inteltip) scope_name: [%s]" % scope) if not "support.function" in scope and not "include_path.pawn" in scope or region.size() > 1 : view.hide_popup() view.add_regions("inteltip", [ ]) return try: if "include_path.pawn" in scope : self.inteltip_include(view, region) else : self.inteltip_function(view, region) except Exception as error: print("on_selection_modified_async exception: %s" % error) def inteltip_include(self, view, region) : location = view.word(region).end() + 1 line = view.substr(view.line(region)) include = includes_re.match(line).group(1) file_name_view = view.file_name() if file_name_view is None: return else: ( file_name, the_include_exists ) = get_file_name( file_name_view, include ) if not the_include_exists : return link_local = file_name + '#' if not '.' in include : link_web = include + '#' include += ".inc" else : link_web = None html = '' html += '

' html += ''+include+'' if link_web : html += ' | WebAPI' html += '
' html += 'Location:
' html += ''+file_name+'' html += '
' view.show_popup(html, 0, location, max_width=700, on_navigate=self.on_navigate) def inteltip_function(self, view, region) : file_name = view.file_name() if not file_name: return word_region = view.word(region) location = word_region.end() + 1 search_func = view.substr(word_region) doctset = set() visited = set() found = None node = nodes[file_name] self.generate_doctset_recur(node, doctset, visited) for func in doctset : if search_func == func.function_name : found = func if found.function_type != FUNC_TYPES.public : break if found: log(4, "param2: [%s]" % simple_escape(found.parameters)) filename = os.path.basename(found.file_name) if found.function_type : if found.return_type : link_local = found.file_name + '#' + FUNC_TYPES(found.function_type).name + ' ' + found.return_type + ':' + found.function_name else : link_local = found.file_name + '#' + FUNC_TYPES(found.function_type).name + ' ' + found.function_name link_web = filename.rsplit('.', 1)[0] + '#' + found.function_name else : link_local = found.file_name + '#' + '^' + found.function_name link_web = '' log( 4, "link_local: %s", link_local ) html = '' html += '
' ############################## TOP html += ''+os.path.basename(found.file_name)+'' if link_web: html += ' | WebAPI' html += '
' ############################## BOTTOM html += ''+FUNC_TYPES(found.function_type).name \ +': '+found.function_name+'' html += '
' html += 'Params: ('+ simple_escape(found.parameters) +')' html += '
' if found.return_type : html += 'Return: '+found.return_type+'' html += '
' ############################## END # log( 1, "html: %s", html ) view.show_popup(html, 0, location, max_width=700, on_navigate=self.on_navigate) view.add_regions("inteltip", [ word_region ], "inteltip.pawn") else: view.hide_popup() view.add_regions("inteltip", [ ]) def on_navigate(self, link) : (file, search) = link.split('#') if "." in file : view = sublime.active_window().open_file(file); def do_position() : if view.is_loading(): sublime.set_timeout(do_position, 100) else : r=view.find(search, 0, sublime.IGNORECASE) view.sel().clear() view.sel().add(r) view.show(r) do_position() else : webbrowser.open_new_tab("http://www.amxmodx.org/api/"+file+"/"+search) def on_activated_async(self, view) : view_size = view.size() log(4, "on_activated_async(2)") log(4, "( on_activated_async ) view.match_selector(0, 'source.sma'): " + str( view.match_selector(0, 'source.sma') )) # log(4, "( on_activated_async ) nodes: " + str( nodes )) log(4, "( on_activated_async ) view.substr(): \n" \ + view.substr( sublime.Region( 0, view_size if view_size < 200 else 200 ) )) if not is_amxmodx_file(view): log(4, "( on_activated_async ) returning on` if not is_amxmodx_file(view)") return if not view.file_name() in nodes : log(4, "( on_activated_async ) returning on` if not view.file_name() in nodes") add_to_queue(view) def on_modified_async(self, view) : self.add_to_queue_delayed(view) def on_post_save_async(self, view) : self.add_to_queue_now(view) def on_load_async(self, view) : self.add_to_queue_now(view) def add_to_queue_now(self, view) : if not is_amxmodx_file(view): return add_to_queue(view) def add_to_queue_delayed(self, view) : if not is_amxmodx_file(view): return if self.delay_queue is not None : self.delay_queue.cancel() if g_delay_time > 0.3: self.delay_queue = Timer( float( g_delay_time ), add_to_queue_forward, [ view ] ) self.delay_queue.start() def on_query_completions(self, view, prefix, locations): """ This is a forward called by Sublime Text when it is about to show the use completions. See: https://www.sublimetext.com/docs/3/api_reference.html#sublime_plugin.ViewEventListener """ view_file_name = view.file_name() if is_amxmodx_file(view): # # # Autocompletion issue # # # https://github.com/evandrocoan/AmxxEditor/issues/9 # # temporarily masking word_separators # # https://github.com/SublimeTextIssues/Core/issues/819 # word_separators = view.settings().get("word_separators") # view.settings().set("word_separators", "") # sublime.set_timeout(lambda: view.settings().set("word_separators", word_separators), 0) if view_file_name is None: view_file_name = str( view.buffer_id() ) # Just in case it is not processed yet if not view_file_name in nodes: log(4, "( on_query_completions ) Adding buffer id " + view_file_name + " in nodes") add_to_queue_forward( view ) # The queue is not processed yet, so there is nothing to show if g_word_autocomplete: log( 16, "(new buffer) Word autocomplete") return None else: log( 16, "(new buffer) Without word autocomplete") return ( [], sublime.INHIBIT_WORD_COMPLETIONS ) if g_word_autocomplete: log( 16, "(Buffer) Word autocomplete + function") return self.generate_funcset( view_file_name, view, prefix, locations ) else: log( 16, "(Buffer) Without word autocomplete + function") return ( self.generate_funcset( view_file_name, view, prefix, locations ), sublime.INHIBIT_WORD_COMPLETIONS ) else: if g_word_autocomplete: log( 16, "(File) Word autocomplete + function") return self.generate_funcset( view_file_name, view, prefix, locations ) else: log( 16, "(File) Without word autocomplete + function") return ( self.generate_funcset( view_file_name, view, prefix, locations ), sublime.INHIBIT_WORD_COMPLETIONS ) log( 16, "No completions") return None def generate_funcset( self, file_name, view, prefix, locations ) : words_list = [] funcs_list = [] funcs_word_list = [] if file_name in nodes: node = nodes[file_name] visited = set() if not view.match_selector(locations[0], 'string') : self.generate_funcset_recur( node, visited, funcs_list, funcs_word_list ) if g_word_autocomplete: start_time = time.time() if len( locations ) > 0: view_words = view.extract_completions( prefix, locations[0] ) else: view_words = view.extract_completions( prefix ) # This view goes first to prioritize matches close to cursor position. for word in view_words: # Remove the annoying `(` on the string word = word.replace('$', '\\$').split('(')[0] if word not in funcs_word_list: words_list.append( ( word, word ) ) if time.time() - start_time > 0.05: break # log( 16, "( generate_funcset ) funcs_list size: %d" % len( funcs_list ) ) # log( 16, "( generate_funcset ) funcs_list items: " + str( sort_nicely( funcs_list ) ) ) return words_list + funcs_list def generate_funcset_recur( self, node, visited, funcs_list, funcs_word_list ) : if node in visited : return visited.add( node ) for child in node.children : self.generate_funcset_recur( child, visited, funcs_list, funcs_word_list ) funcs_list.extend( node.funcs_list ) funcs_word_list.extend( node.words_list ) def generate_doctset_recur(self, node, doctset, visited) : if node in visited : return visited.add(node) for child in node.children : self.generate_doctset_recur(child, doctset, visited) doctset.update(node.doct) def is_amxmodx_file(view) : return view.match_selector(0, 'source.sma') def on_settings_modified(): log(4, "on_settings_modified" ) global g_enable_inteltip global g_new_file_syntax global g_word_autocomplete global g_function_autocomplete settings = sublime.load_settings("%s.sublime-settings" % CURRENT_PACKAGE_NAME) invalid = is_invalid_settings(settings) if invalid: if not g_is_package_loading: sublime.message_dialog("AmxxEditor:\n\n" + invalid) g_enable_inteltip = 0 return # check package path packages_path = os.path.join( sublime.packages_path(), CURRENT_PACKAGE_NAME ) if not os.path.isdir(packages_path) : os.mkdir(packages_path) # fix-path fix_path(settings, 'include_directory') # Get the set color scheme popup_color_scheme = settings.get('popup_color_scheme') # popUp.CSS global g_inteltip_style g_inteltip_style = sublime.load_resource("Packages/%s/%s-popup.css" % (CURRENT_PACKAGE_NAME, popup_color_scheme)) g_inteltip_style = g_inteltip_style.replace("\r", "") # fix win/linux newlines # cache setting global g_enable_buildversion, g_delay_time, g_add_paremeters g_enable_inteltip = settings.get('enable_inteltip', True) g_enable_buildversion = settings.get('enable_buildversion', False) g_word_autocomplete = settings.get('word_autocomplete', False) g_function_autocomplete = settings.get('function_autocomplete', False) g_new_file_syntax = settings.get('amxx_file_syntax', g_new_file_syntax) log.debug_level = settings.get('debug_level', 1) g_delay_time = settings.get('live_refresh_delay', 1.0) g_add_paremeters = settings.get('add_function_parameters', False) g_include_dir.clear() include_directory = settings.get('include_directory', './include') if isinstance( include_directory, list ): for path in include_directory: real_path = os.path.realpath( path ) if os.path.isdir( real_path ): g_include_dir.add( real_path ) else: real_path = os.path.realpath( include_directory ) if os.path.isdir( real_path ): g_include_dir.add( real_path ) file_observer.unschedule_all() log(4, "( on_settings_modified ) debug_level: %d", log.debug_level) log(4, "( on_settings_modified ) g_include_dir: %s", g_include_dir) log(4, "( on_settings_modified ) g_add_paremeters: %s", g_add_paremeters) for directory in g_include_dir: file_observer.schedule( file_event_handler, directory, True ) def is_invalid_settings(settings): general_error = "You are not set correctly settings for AmxxEditor.\n\n" setting_names = [ "include_directory", "popup_color_scheme", "amxx_file_syntax" ] for setting_name in setting_names: result = general_settings_checker( settings, setting_name, general_error ) if result: return result path_prefix = "" setting_name = "include_directory" default_value = "F:\\SteamCMD\\steamapps\\common\\Half-Life\\czero\\addons\\amxmodx\\scripting\\include" checker = lambda file_path: os.path.exists( file_path ) result = path_settings_checker( settings, setting_name, default_value, path_prefix, checker ) if result: return result path_prefix = os.path.dirname( sublime.packages_path() ) setting_name = "amxx_file_syntax" default_value = g_new_file_syntax checker = lambda file_path: os.path.exists( file_path ) or is_inside_sublime_package( file_path ) result = path_settings_checker( settings, setting_name, default_value, path_prefix, checker ) if result: return result def general_settings_checker(settings, setting_name, general_error): setting_value = settings.get( setting_name ) if setting_value is None: return general_error + "Missing `%s` value." % setting_name def path_settings_checker(settings, setting_name, default_value, prefix_path, checker): setting_value = settings.get( setting_name ) if setting_value != default_value: full_path = os.path.normpath( os.path.join( prefix_path, setting_value ) ) if not checker( full_path ): lines = \ [ "The setting `%s` is not configured correctly. The following path does not exists:\n\n" % setting_name, "%s (%s)" % (setting_value, full_path), "\n\nPlease, go to the following menu and fix the setting:\n\n" "`AmxxEditor -> Configure AMXX-Autocompletion Settings`\n\n", "`Preferences -> Packages Settings -> AmxxEditor -> Configure AMXX-Autocompletion Settings`", ] text = "".join( lines ) print( "\n" + text.replace( "\n\n", "\n" ) ) return text def is_inside_sublime_package(file_path): try: packages_start = file_path.find( "Packages" ) packages_relative_path = file_path[packages_start:].replace( "\\", "/" ) # log( 1, "is_inside_sublime_package, packages_relative_path: " + str( packages_relative_path ) ) sublime.load_binary_resource( packages_relative_path ) return True except IOError: return False def fix_path(settings, key) : org_path = settings.get(key) if org_path is "${file_path}" : return path = os.path.normpath(org_path) if os.path.isdir(path): path += '/' settings.set(key, path) def sort_nicely( words_set ): """ Sort the given iterable in the way that humans expect. """ convert = lambda text: int(text) if text.isdigit() else text alphanum_key = lambda key: [ convert(c) for c in re.split('([0-9]+)', key[0]) ] return sorted( words_set, key = alphanum_key ) def add_to_queue_forward(view) : if g_delay_time > 0.3: sublime.set_timeout_async( lambda: add_to_queue( view ), float( g_delay_time ) * 1000.0 ) def add_to_queue(view) : """ The view can only be accessed from the main thread, so run the regex now and process the results later """ log( 4, "( add_to_queue ) view.file_name(): %s", view.file_name() ) # When the view is not saved, we need to use its buffer id, instead of its file name. view_file_name = view.file_name() if view_file_name is None : view_file_name = str( view.buffer_id() ) if view_file_name not in processingSetQueueSet: processingSetQueueSet.add( view_file_name ) processingSetQueue.put( ( view_file_name, view.substr( sublime.Region( 0, view.size() ) ) ) ) include_directory = os.path.realpath( os.path.join( os.path.dirname( view_file_name ), "include" ) ) if include_directory not in g_include_dir: if os.path.isdir( include_directory ): g_include_dir.add( include_directory ) file_observer.schedule( file_event_handler, include_directory, True ) def add_include_to_queue(file_name) : if file_name not in processingSetQueueSet: processingSetQueueSet.add( file_name ) processingSetQueue.put((file_name, None)) class IncludeFileEventHandler(watchdog.events.FileSystemEventHandler) : def __init__(self) : watchdog.events.FileSystemEventHandler.__init__(self) def on_created(self, event) : sublime.set_timeout(lambda: on_modified_main_thread(event.src_path), 0) def on_modified(self, event) : sublime.set_timeout(lambda: on_modified_main_thread(event.src_path), 0) def on_deleted(self, event) : sublime.set_timeout(lambda: on_deleted_main_thread(event.src_path), 0) def on_modified_main_thread(file_path) : if not is_active(file_path) : add_include_to_queue(file_path) def on_deleted_main_thread(file_path) : if is_active(file_path) : return node = nodes.get(file_path) if node is None : return node.remove_all_children_and_funcs() def is_active(file_name) : return sublime.active_window().active_view().file_name() == file_name class ProcessQueueThread(watchdog.utils.DaemonThread) : def run(self) : while self.should_keep_running() : (file_name, view_buffer) = processingSetQueue.get() try: processingSetQueueSet.remove( file_name ) except: pass # When the `view_buffer` is None, it means we are processing a file on the disk, instead # of a file on an Sublime Text View (its text buffer). if view_buffer is None : self.process_existing_include(file_name) else : self.process(file_name, view_buffer) def process(self, view_file_name, view_buffer) : base_includes = set() (current_node, node_added) = get_or_add_node(view_file_name) # Here we parse the text file to know which modules it is including. includes = includes_re.findall(view_buffer) # Now for each module it is including we load that include file to the autocomplete list. for include in includes: self.load_from_file(view_file_name, include, current_node, current_node, base_includes) # For each module it was loaded but it not present on the current file we just switched, # we remove that include file to the autocomplete list. for removed_node in current_node.children.difference(base_includes) : current_node.remove_child(removed_node) # To process the current file functions for autocomplete process_buffer(view_buffer, current_node) def process_existing_include(self, file_name) : current_node = nodes.get(file_name) if current_node is None or not os.path.exists( file_name ): return base_includes = set() with open(file_name, 'r', errors="ignore") as f : log(2, "(analyzer) Processing Include File %s" % file_name) includes = includes_re.findall(f.read()) for include in includes: self.load_from_file(file_name, include, current_node, current_node, base_includes) for removed_node in current_node.children.difference(base_includes) : current_node.remove_child(removed_node) process_include_file(current_node) def load_from_file(self, view_file_name, base_file_name, parent_node, base_node, base_includes) : (file_name, exists) = get_file_name(view_file_name, base_file_name) if not exists : log(1, "(analyzer) Include File Not Found: %s" % base_file_name) return (node, node_added) = get_or_add_node(file_name) parent_node.add_child(node) if parent_node == base_node : base_includes.add(node) if not node_added : return with open(file_name, 'r', errors="ignore") as f : log(2, "(analyzer) Processing Include File %s" % file_name) includes = includes_re.findall(f.read()) for include in includes : self.load_from_file(view_file_name, include, node, base_node, base_includes) process_include_file(node) def get_file_name(view_file_name, base_file_name) : log(4, "g_include_dir: %s", g_include_dir) path_exists = False # True, if `base_file_name` is a include file name, instead of full file path if local_re.search(base_file_name) == None: for directory in g_include_dir: file_name = os.path.join(directory, base_file_name + '.inc') if os.path.exists(file_name): path_exists = True break else: file_name = os.path.join(os.path.dirname(view_file_name), base_file_name) path_exists = os.path.exists(file_name) return (file_name, path_exists) def get_or_add_node(file_name) : """ Here if `file_name` is a buffer id as a string, I just check if the buffer exists. However if it is a file name, I need to check if its a buffer id is present here, and if so, I must to remove it and create a new node with the file name. This is necessary because the file could be just create, parsed and then saved. Therefore after did so, we need to keep reusing its buffer. But as it is saved we are using its file name instead of its buffer id, then we need to remove the buffer id in order to avoid duplicated entries. Though I am not implementing this here to save time and performance """ node = nodes.get(file_name) if node is None : node = Node(file_name) nodes[file_name] = node return (node, True) return (node, False) # ============= NEW CODE ------------------------------------------------------------------------------------------------------------ class TooltipDocumentation(object): def __init__(self, function_name, parameters, file_name, function_type, return_type): """ For `function_type` see FUNC_TYPES. """ self.function_name = function_name self.parameters = parameters self.file_name = file_name self.function_type = function_type self.return_type = return_type class Node(object): def __init__(self, file_name) : self.file_name = file_name self.doct = set() self.children = set() self.parents = set() # They are list to keep ordering self.funcs_list = [] self.words_list = [] self.words_set = set() try: float(file_name) self.isFromBufferOnly = True except ValueError: self.isFromBufferOnly = False def add_child(self, node) : self.children.add(node) node.parents.add(self) def remove_child(self, node) : self.children.remove(node) node.parents.remove(self) if len(node.parents) <= 0 : nodes.pop(node.file_name) def remove_all_children_and_funcs(self) : for child in self.children : self.remove_child(node) self.doct.clear() self.funcs_list.clear() self.words_list.clear() class TextReader(object): def __init__(self, text): self.text = text.splitlines() self.position = -1 def readline(self) : self.position += 1 if self.position < len(self.text) : retval = self.text[self.position] if retval == '' : return '\n' else : return retval else : return '' class PawnParse(object): def __init__(self) : self.node = None self.isTheCurrentFile = False self.save_const_timer = None self.constants_count = 0 def start( self, pFile, node, isTheCurrentFile=False ) : """ When the buffer is not None, it is always the current file. """ log(8, "(analyzer) CODE PARSE Start [%s]" % node.file_name) self.isTheCurrentFile = isTheCurrentFile self.file = pFile self.file_name = os.path.basename(node.file_name) self.node = node self.found_comment = False self.found_enum = False self.is_to_skip_brace = False self.enum_contents = '' self.brace_level = 0 self.restore_buffer = None self.is_to_skip_next_line = False self.if_define_brace_level = 0 self.else_defined_brace_level = 0 self.if_define_level = 0 self.else_define_level = 0 self.is_on_if_define = [] self.is_on_else_define = [] self.node.doct.clear() self.node.funcs_list.clear() self.node.words_list.clear() self.node.words_set.clear() self.start_parse() if self.constants_count != len(g_constants_list) : if self.save_const_timer : self.save_const_timer.cancel() self.save_const_timer = Timer(4.0, self.save_constants) self.save_const_timer.start() log(8, "(analyzer) CODE PARSE End [%s]" % node.file_name) def save_constants(self) : self.save_const_timer = None self.constants_count = len(g_constants_list) windows = sublime.windows() # If you have a project within 10000 files, each time this is updated, will for sublime to # process again all the files. Therefore only allow this on project with no files to index. # # If someone is calling this, it means there are some windows with a AMXX file open. Therefore # we do not care to check whether that window has a project or not and there will always be # constants to save. for window in windows: # log(4, "(save_constants) window.id(): " + str( window.id() ) ) # log(4, "(save_constants) window.folders(): " + str( window.folders() ) ) # log(4, "(save_constants) window.project_file_name(): " + str( window.project_file_name() ) ) if len( window.folders() ) > 0: log( 4, "(save_constants) Not saving this time." ) return constants = "___test" for const in g_constants_list : constants += "|" + const syntax = "%YAML 1.2\n---\nscope: source.sma\nhidden: true\ncontexts:\n main:\n - match: \\b(" \ + constants + ")\\b\s*(?!\()\n scope: constant.vars.pawn\n\n" file_name = os.path.join(sublime.packages_path(), CURRENT_PACKAGE_NAME, "AmxxEditorConsts.sublime-syntax") f = open(file_name, 'w', errors="ignore") f.write(syntax) f.close() log(8, "(analyzer) call save_constants()") def read_line(self) : if self.restore_buffer : line = self.restore_buffer self.restore_buffer = None else : line = self.file.readline() if len(line) > 0 : return line else : return None def read_string(self, current_line) : current_line = current_line.replace('\t', ' ').strip() while ' ' in current_line : current_line = current_line.replace(' ', ' ') current_line = current_line.lstrip() result = '' i = 0 # log( 1, str( current_line ) ) buffer_length = len(current_line) while i < buffer_length : if current_line[i] == '/' and i + 1 < len(current_line): if current_line[i + 1] == '/' : self.brace_level += result.count('{') - result.count('}') return result elif current_line[i + 1] == '*' : self.found_comment = True i += 1 elif not self.found_comment : result += '/' elif self.found_comment : if current_line[i] == '*' and i + 1 < len(current_line) and current_line[i + 1] == '/' : self.found_comment = False i += 1 elif not (i > 0 and current_line[i] == ' ' and current_line[i - 1] == ' '): result += current_line[i] i += 1 self.brace_level += result.count('{') - result.count('}') return result def skip_function_block(self, current_line) : inChar = False inString = False num_brace = 0 current_line = current_line + ' ' self.is_to_skip_brace = False while current_line is not None and current_line.isspace() : current_line = self.read_line() while current_line is not None : # log( 32, "skip_function_block: " + current_line ) i = 0 pos = 0 lastChar = '' penultimateChar = '' for c in current_line : i += 1 if not inString and not inChar and lastChar == '*' and c == '/' : self.found_comment = False if not inString and not inChar and self.found_comment: penultimateChar = lastChar lastChar = c continue if not inString and not inChar and lastChar == '/' and c == '*' : self.found_comment = True penultimateChar = lastChar lastChar = c continue if not inString and not inChar and c == '/' and lastChar == '/' : break if c == '"' : if inString and lastChar != '^' : inString = False else : inString = True if not inString and c == '\'' : if inChar and lastChar != '^' : inChar = False else : inChar = True # This is hard stuff. We need to fix the parsing for the following problem: # # public on_damage(id) # { # #if defined DAMAGE_RECIEVED # if ( is_user_connected(id) && is_user_connected(attacker) ) # { # #else # if ( is_user_connected(attacker) ) # { # #endif # } # return PLUGIN_CONTINUE # } # public death_hook() # { # { # new kuid = get_user_userid(killer) # } # } # # Above here we may notice, there are 2 braces opening but only one brace close. # Therefore, we will skip the rest of the source code if we do not handle the braces # definitions between the `#if` and `#else` macro clauses. # # To keep track about where we are, we need to keep track about how much braces # levels are being opened and closed using the variables `self.if_define_brace_level` # and `self.else_defined_brace_level`. And finally at the end of it all on the `#endif`, # we update the `num_brace` with the correct brace level. # if not inString and not inChar : # Flags when we enter and leave the `#if ... #else ... #endif` blocks if penultimateChar == '#': # Cares of `#if` if lastChar == 'i' and c == 'f': ++self.if_define_level self.is_on_if_define.append( True ) # Cares of `#else` and `#end` elif lastChar == 'e': if c == 'l': ++self.else_define_level self.is_on_if_define.append( False ) self.is_on_else_define.append( True ) elif c == 'n': # Decrement the `#else` level, only if it exists if len( self.is_on_if_define ) > 0: if not self.is_on_if_define[ -1 ]: self.is_on_if_define.pop() if len( self.is_on_else_define ) > 0: --self.else_define_level self.is_on_else_define.pop() if len( self.is_on_if_define ) > 0: --self.if_define_level self.is_on_if_define.pop() # If there are unclosed levels on the preprocessor, fix the `num_brace` level extra_levels = max( self.else_defined_brace_level, self.if_define_brace_level ) num_brace -= extra_levels # Both must to be equals, so just reset their levels. self.if_define_brace_level -= extra_levels self.else_defined_brace_level -= extra_levels # Flags when we enter and leave the braces `{ ... }` blocks if c == '{': num_brace += 1 self.is_to_skip_brace = True if len( self.is_on_if_define ) > 0: if self.is_on_if_define[ -1 ] : self.if_define_brace_level += 1 else: self.else_defined_brace_level += 1 elif c == '}': pos = i num_brace -= 1 if len( self.is_on_if_define ) > 0: if self.is_on_if_define[ -1 ] : self.if_define_brace_level -= 1 else: self.else_defined_brace_level -= 1 penultimateChar = lastChar lastChar = c # log( 32, "num_brace: %d" % num_brace ) # log( 32, "if_define_brace_level: %d" % self.if_define_brace_level ) # log( 32, "else_defined_brace_level: %d" % self.else_defined_brace_level ) # log( 32, "is_on_if_define: " + str( self.is_on_if_define ) ) # log( 32, "is_on_else_define: " + str( self.is_on_else_define ) ) # log( 32, "" ) if num_brace == 0 : self.restore_buffer = current_line[pos:] return current_line = self.read_line() def is_valid_name(self, name) : if not name or not name[0].isalpha() and name[0] != '_' : return False return re.match('^[\w_]+$', name) is not None def add_constant(self, name) : fixname = re.search('(\\w*)', name) if fixname : name = fixname.group(1) g_constants_list.add(name) def add_enum(self, current_line) : current_line = current_line.strip() if current_line == '' : return split = current_line.split('[') self.add_constant(split[0]) self.add_general_autocomplete(current_line, 'enum', split[0]) log(8, "(analyzer) parse_enum add: [%s] -> [%s]" % (current_line, split[0])) def add_general_autocomplete(self, name, info, autocomplete) : if name not in self.node.words_set: self.node.words_set.add( name ) self.node.words_list.append( name ) if self.node.isFromBufferOnly or self.isTheCurrentFile: self.node.funcs_list.append( ["{}\t {}".format( name, info ), autocomplete] ) else: self.node.funcs_list.append( ["{} \t{} - {}".format( name, self.file_name, info ), autocomplete] ) def add_function_autocomplete(self, name, info, autocomplete, param_count) : show_name = name + "(" + str( param_count ) + ")" self.node.words_set.add( name ) self.node.words_list.append( name ) # We do not check whether `if name in words` because we can have several functions # with the same name but different parameters if self.node.isFromBufferOnly or self.isTheCurrentFile: self.node.funcs_list.append( ["{}\t {}".format( show_name, info ), autocomplete] ) else: self.node.funcs_list.append( ["{} \t{} - {}".format( show_name, self.file_name, info ), autocomplete] ) def add_word_autocomplete(self, name) : """ Used to add a word to the auto completion of the current current_line. Therefore, it does not need the file name as the auto completion for words from other files/sources. """ if name not in self.node.words_list: self.node.words_set.add( name ) self.node.words_list.append( name ) if self.isTheCurrentFile: self.node.funcs_list.append( [name, name] ) else: self.node.funcs_list.append( ["{}\t {}".format( name, self.file_name ), name] ) def start_parse(self) : while True : current_line = self.read_line() # log( 1, str( current_line ) ) if current_line is None : break current_line = self.read_string(current_line) if len(current_line) <= 0 : continue #if "sma" in self.node.file_name : # print("read: skip:[%d] brace_level:[%d] buff:[%s]" % (self.is_to_skip_next_line, self.brace_level, current_line)) if self.is_to_skip_next_line : self.is_to_skip_next_line = False continue if current_line.startswith('#pragma deprecated') : current_line = self.read_line() if current_line is not None and current_line.startswith('stock ') : self.skip_function_block(current_line) elif current_line.startswith('#define ') : current_line = self.parse_define(current_line) elif current_line.startswith('const ') : current_line = self.parse_const(current_line) elif current_line.startswith('enum ') or current_line == 'enum': self.found_enum = True self.enum_contents = '' elif current_line.startswith('new ') : self.parse_variable(current_line) elif current_line.startswith('public ') : self.parse_function(current_line, 1) elif current_line.startswith('stock ') : """ new STOCK_TEST1[] = "something"; const STOCK_TEST2[] = "something"; stock STOCK_TEST3[] = "something"; stock const STOCK_TEST4[] = "something"; stock bool:xs_vec_equal(const Float:vec1[], const Float:vec2[]) { } stock xs_vec_add(const Float:in1[], const Float:in2[], Float:out[]) { } """ if current_line.split('(')[0].find(" const ") > -1: current_line = current_line[6:] self.parse_const(current_line) else: matches = function_re.search(current_line) log( 8, 'current_line: %s', current_line ) log( 8, 'matches: %s', matches ) if matches == None: current_line = "new " + current_line[6:] self.parse_variable(current_line) else: self.parse_function(current_line, 2) elif current_line.startswith('forward ') : self.parse_function(current_line, 3) elif current_line.startswith('native ') : self.parse_function(current_line, 4) elif not self.found_enum and not current_line[0] == '#' : self.parse_function(current_line, 0) if self.found_enum : self.parse_enum(current_line) def parse_define(self, current_line) : define = re.search('#define[\\s]+([^\\s]+)[\\s]+(.+)', current_line) if define : current_line = '' name = define.group(1) value = define.group(2).strip() count = 0 params = name.split('(') name = params[0] params_count = 0 if len( params ) == 2: params = params[1].split(',') comma_count = len( params ) params_count = comma_count # If we entered here, there are at least one parameter params = "${1:param1}" items = range( 2, comma_count + 1 ) for item in items: params += ", " + '${%d:param%d}' % ( item, item ) else: params = "" if params_count > 0: self.add_function_autocomplete( name, 'define: ' + value, name + "(" + params + ")", params_count ) else: self.add_general_autocomplete( name, 'define: ' + value, name ) self.add_constant( name ) log(8, "(analyzer) parse_define add: [%s]" % name) def parse_const(self, current_line) : current_line = current_line[6:] log(8, "(analyzer) current_line: [%s]" % current_line) split = current_line.split('=', 1) if len(split) < 2 : return name = split[0].strip() value = split[1].strip() newline = value.find(';') if (newline != -1) : self.restore_buffer = value[newline+1:].strip() value = value[0:newline] self.add_constant(name) self.add_general_autocomplete(name, 'const: ' + value, name) log(8, "(analyzer) parse_const add: [%s]" % name) def parse_variable(self, current_line) : if current_line.startswith('new const ') : current_line = current_line[10:] else : current_line = current_line[4:] varName = "" lastChar = '' i = 0 pos = 0 num_brace = 0 multiLines = True skipSpaces = False parseName = True inBrackets = False inBraces = False inString = False while multiLines : multiLines = False for c in current_line : i += 1 if (c == '"') : if (inString and lastChar != '^') : inString = False else : inString = True if (inString == False) : if (c == '{') : num_brace += 1 inBraces = True elif (c == '}') : num_brace -= 1 if (num_brace == 0) : inBraces = False if skipSpaces : if c.isspace() : continue else : skipSpaces = False parseName = True if parseName : if (c == ':') : varName = '' elif (c == ' ' or c == '=' or c == ';' or c == ',') : varName = varName.strip() if (varName != '') : self.add_word_autocomplete( varName ) log(8, "(analyzer) parse_variable add: [%s]" % varName) varName = '' parseName = False inBrackets = False elif (c == '[') : inBrackets = True elif (inBrackets == False) : varName += c if (inString == False and inBrackets == False and inBraces == False) : if not parseName and c == ';' : self.restore_buffer = current_line[i:].strip() return if (c == ',') : skipSpaces = True lastChar = c if (c != ',') : varName = varName.strip() if varName != '' : self.add_word_autocomplete( varName ) log(8, "(analyzer) parse_variable add: [%s]" % varName) else : multiLines = True current_line = ' ' while current_line is not None and current_line.isspace() : current_line = self.read_line() def parse_enum(self, current_line) : pos = current_line.find('}') if pos != -1 : current_line = current_line[0:pos] self.found_enum = False self.enum_contents = '%s\n%s' % (self.enum_contents, current_line) current_line = '' ignore = False if not self.found_enum : pos = self.enum_contents.find('{') self.enum_contents = self.enum_contents[pos + 1:] for c in self.enum_contents : if c == '=' or c == '#' : ignore = True elif c == '\n': ignore = False elif c == ':' : current_line = '' continue elif c == ',' : self.add_enum(current_line) current_line = '' ignore = False continue if not ignore : current_line += c self.add_enum(current_line) current_line = '' def parse_function(self, current_line, type) : multi_line = False temp = '' full_func_str = None open_paren_found = False while current_line is not None : current_line = current_line.strip() if not open_paren_found : parenpos = current_line.find('(') if parenpos == -1 : return open_paren_found = True if open_paren_found : pos = current_line.find(')') if pos != -1 : full_func_str = current_line[0:pos + 1] current_line = current_line[pos+1:] if (multi_line) : full_func_str = '%s%s' % (temp, full_func_str) break multi_line = True temp = '%s%s' % (temp, current_line) current_line = self.read_line() if current_line is None : return current_line = self.read_string(current_line) if full_func_str is not None : error = self.parse_function_params(full_func_str, type) if not error and type <= 2 : self.skip_function_block(current_line) if not self.is_to_skip_brace : self.is_to_skip_next_line = True #print("skip_brace: error:[%d] type:[%d] found:[%d] skip:[%d] func:[%s]" % (error, type, self.is_to_skip_brace, self.is_to_skip_next_line, full_func_str)) def parse_function_params(self, func, function_type) : if function_type == 0 : remaining = func else : split = func.split(' ', 1) remaining = split[1] split = remaining.split('(', 1) if len(split) < 2 : log(4, "(analyzer) parse_params return1: [%s]" % split) return 1 remaining = split[1] returntype = '' funcname_and_return = split[0].strip() split_funcname_and_return = funcname_and_return.split(':') if len(split_funcname_and_return) > 1 : funcname = split_funcname_and_return[1].strip() returntype = split_funcname_and_return[0].strip() else : funcname = split_funcname_and_return[0].strip() if funcname.startswith("operator") : return 0 if not self.is_valid_name(funcname) : log(4, "(analyzer) parse_params invalid name: [%s]" % funcname) return 1 remaining = remaining.strip() if remaining == ')': params = [] else: params = remaining.strip()[:-1].split(',') if g_add_paremeters: i = 1 autocomplete = funcname + '(' for param in params: if i > 1: autocomplete += ', ' autocomplete += '${%d:%s}' % (i, param.strip()) i += 1 autocomplete += ')' else: autocomplete = funcname + "()" self.add_function_autocomplete(funcname, FUNC_TYPES(function_type).name, autocomplete, len( params )) self.node.doct.add(TooltipDocumentation(funcname, func[func.find("(")+1:-1], self.node.file_name, function_type, returntype)) log(8, "(analyzer) parse_params add: [%s]" % func) return 0 def process_buffer(text, node) : if g_function_autocomplete: text_reader = TextReader(text) pawnParse.start(text_reader, node, True) def process_include_file(node) : with open(node.file_name, errors="ignore") as file : pawnParse.start(file, node) def simple_escape(html) : return html.replace('&', '&') pawnParse = PawnParse() process_thread = ProcessQueueThread() file_event_handler = IncludeFileEventHandler() ```