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 += '
' ############################## 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()
```
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: '
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 += ' ' ############################## 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()
```
' html += ''+file_name+'' html += '
' html += 'Params: ('+ simple_escape(found.parameters) +')' html += '
' if found.return_type : html += 'Return: '+found.return_type+'' html += '