247 lines
8.0 KiB
Python
247 lines
8.0 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
This module contains the worker functions/classes used on the server side.
|
|
|
|
A worker is a function or a callable which receive one single argument (the
|
|
decoded json object) and returns a tuple made up of a status (bool) and a
|
|
response object (json serializable).
|
|
|
|
A worker is always tightly coupled with its caller, so are the data.
|
|
|
|
.. warning::
|
|
This module should keep its dependencies as low as possible and fully
|
|
supports python2 syntax. This is badly needed since the server might be run
|
|
with a python2 interpreter. We don't want to force the user to install all
|
|
the pyqode dependencies twice (if the user choose to run the server with
|
|
python2, which might happen in pyqode.python to support python2 syntax).
|
|
|
|
"""
|
|
import logging
|
|
import re
|
|
import sys
|
|
import traceback
|
|
|
|
|
|
def echo_worker(data):
|
|
"""
|
|
Example of worker that simply echoes back the received data.
|
|
|
|
:param data: Request data dict.
|
|
:returns: True, data
|
|
"""
|
|
print('echo worker running')
|
|
return data
|
|
|
|
|
|
class CodeCompletionWorker(object):
|
|
"""
|
|
This is the worker associated with the code completion mode.
|
|
|
|
The worker does not actually do anything smart, the real work of collecting
|
|
code completions is accomplished by the completion providers (see the
|
|
:class:`pyqode.core.backend.workers.CodeCompletionWorker.Provider`
|
|
interface) listed in
|
|
:attr:`pyqode.core.backend.workers.CompletionWorker.providers`.
|
|
|
|
Completion providers must be installed on the CodeCompletionWorker
|
|
at the beginning of the main server script, e.g.::
|
|
|
|
from pyqode.core.backend import CodeCompletionWorker
|
|
CodeCompletionWorker.providers.insert(0, MyProvider())
|
|
"""
|
|
#: The list of code completion provider to run on each completion request.
|
|
providers = []
|
|
|
|
class Provider(object):
|
|
"""
|
|
This class describes the expected interface for code completion
|
|
providers.
|
|
|
|
You can inherit from this class but this is not required as long as you
|
|
implement a ``complete`` method which returns the list of completions
|
|
and have the expected signature::
|
|
|
|
def complete(self, code, line, column, path, encoding, prefix):
|
|
pass
|
|
|
|
"""
|
|
|
|
def complete(self, code, line, column, path, encoding, prefix):
|
|
"""
|
|
Returns a list of completions.
|
|
|
|
A completion is dictionary with the following keys:
|
|
|
|
- 'name': name of the completion, this the text displayed and
|
|
inserted when the user select a completion in the list
|
|
- 'icon': an optional icon file name
|
|
- 'tooltip': an optional tooltip string
|
|
|
|
:param code: code string
|
|
:param line: line number (0 based)
|
|
:param column: column number (0 based)
|
|
:param path: file path
|
|
:param encoding: file encoding
|
|
:param prefix: completion prefix (text before cursor)
|
|
|
|
:returns: A list of completion dicts as described above.
|
|
:rtype: list
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def __call__(self, data):
|
|
"""
|
|
Do the work (this will be called in the child process by the
|
|
SubprocessServer).
|
|
"""
|
|
code = data['code']
|
|
line = data['line']
|
|
column = data['column']
|
|
path = data['path']
|
|
encoding = data['encoding']
|
|
prefix = data['prefix']
|
|
req_id = data['request_id']
|
|
completions = []
|
|
for prov in CodeCompletionWorker.providers:
|
|
try:
|
|
results = prov.complete(
|
|
code, line, column, path, encoding, prefix)
|
|
completions.append(results)
|
|
if len(completions):
|
|
break
|
|
except:
|
|
sys.stderr.write('Failed to get completions from provider %r'
|
|
% prov)
|
|
exc1, exc2, exc3 = sys.exc_info()
|
|
traceback.print_exception(exc1, exc2, exc3, file=sys.stderr)
|
|
return [(line, column, req_id)] + completions
|
|
|
|
|
|
class DocumentWordsProvider(object):
|
|
"""
|
|
Provides completions based on the document words
|
|
"""
|
|
words = {}
|
|
|
|
# word separators
|
|
separators = [
|
|
'~', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '+', '{',
|
|
'}', '|', ':', '"', "'", "<", ">", "?", ",", ".", "/", ";", '[',
|
|
']', '\\', '\n', '\t', '=', '-', ' '
|
|
]
|
|
|
|
@staticmethod
|
|
def split(txt, seps):
|
|
"""
|
|
Splits a text in a meaningful list of words based on a list of word
|
|
separators (define in pyqode.core.settings)
|
|
|
|
:param txt: Text to split
|
|
:param seps: List of words separators
|
|
:return: A **set** of words found in the document (excluding
|
|
punctuations, numbers, ...)
|
|
"""
|
|
# replace all possible separators with a default sep
|
|
default_sep = seps[0]
|
|
for sep in seps[1:]:
|
|
if sep:
|
|
txt = txt.replace(sep, default_sep)
|
|
# now we can split using the default_sep
|
|
raw_words = txt.split(default_sep)
|
|
words = set()
|
|
for word in raw_words:
|
|
# w = w.strip()
|
|
if word.replace('_', '').isalpha():
|
|
words.add(word)
|
|
return sorted(words)
|
|
|
|
def complete(self, code, *args):
|
|
"""
|
|
Provides completions based on the document words.
|
|
|
|
:param code: code to complete
|
|
:param args: additional (unused) arguments.
|
|
"""
|
|
completions = []
|
|
for word in self.split(code, self.separators):
|
|
completions.append({'name': word})
|
|
return completions
|
|
|
|
|
|
def finditer_noregex(string, sub, whole_word):
|
|
"""
|
|
Search occurrences using str.find instead of regular expressions.
|
|
|
|
:param string: string to parse
|
|
:param sub: search string
|
|
:param whole_word: True to select whole words only
|
|
"""
|
|
start = 0
|
|
while True:
|
|
start = string.find(sub, start)
|
|
if start == -1:
|
|
return
|
|
if whole_word:
|
|
if start:
|
|
pchar = string[start - 1]
|
|
else:
|
|
pchar = ' '
|
|
try:
|
|
nchar = string[start + len(sub)]
|
|
except IndexError:
|
|
nchar = ' '
|
|
if nchar in DocumentWordsProvider.separators and \
|
|
pchar in DocumentWordsProvider.separators:
|
|
yield start
|
|
start += len(sub)
|
|
else:
|
|
yield start
|
|
start += 1
|
|
|
|
|
|
def findalliter(string, sub, regex=False, case_sensitive=False,
|
|
whole_word=False):
|
|
"""
|
|
Generator that finds all occurrences of ``sub`` in ``string``
|
|
:param string: string to parse
|
|
:param sub: string to search
|
|
:param regex: True to search using regex
|
|
:param case_sensitive: True to match case, False to ignore case
|
|
:param whole_word: True to returns only whole words
|
|
:return:
|
|
"""
|
|
if not sub:
|
|
return
|
|
if regex:
|
|
flags = re.MULTILINE
|
|
if not case_sensitive:
|
|
flags |= re.IGNORECASE
|
|
for val in re.finditer(sub, string, flags):
|
|
yield val.span()
|
|
else:
|
|
if not case_sensitive:
|
|
string = string.lower()
|
|
sub = sub.lower()
|
|
for val in finditer_noregex(string, sub, whole_word):
|
|
yield val, val + len(sub)
|
|
|
|
|
|
def findall(data):
|
|
"""
|
|
Worker that finds all occurrences of a given string (or regex)
|
|
in a given text.
|
|
|
|
:param data: Request data dict::
|
|
{
|
|
'string': string to search in text
|
|
'sub': input text
|
|
'regex': True to consider string as a regular expression
|
|
'whole_word': True to match whole words only.
|
|
'case_sensitive': True to match case, False to ignore case
|
|
}
|
|
:return: list of occurrence positions in text
|
|
"""
|
|
return list(findalliter(
|
|
data['string'], data['sub'], regex=data['regex'],
|
|
whole_word=data['whole_word'], case_sensitive=data['case_sensitive']))
|