Esempio n. 1
0
class RLangIntel(CitadelLangIntel, ParenStyleCalltipIntelMixin,
                   ProgLangTriggerIntelMixin):
    lang = lang

    # Used by ProgLangTriggerIntelMixin.preceding_trg_from_pos()
    #trg_chars = tuple('$@[( ')
    #calltip_trg_chars = tuple('(,')

    # named styles used by the class
    whitespace_style = SCE_UDL_SSL_DEFAULT
    operator_style   = SCE_UDL_SSL_OPERATOR
    identifier_style = SCE_UDL_SSL_IDENTIFIER
    keyword_style    = SCE_UDL_SSL_WORD
    variable_style   = SCE_UDL_SSL_VARIABLE
    string_style     = SCE_UDL_SSL_STRING
    comment_styles   = (SCE_UDL_SSL_COMMENT, SCE_UDL_SSL_COMMENTBLOCK)
    comment_styles_or_whitespace = comment_styles + (whitespace_style, )
    word_styles      = (variable_style, identifier_style, keyword_style)
    ##
    namespace_style  = keyword_style
    argument_style   = variable_style
    element_style    = variable_style
    varname_styles   = (identifier_style, keyword_style)
    
    # R functions that accept graphical parameters (par)
    func_graphics = ('plot', 'boxplot', 'bxp', 'box', 'curve', 'line', 'points',
                'text', 'mtext', 'title', 'image',)

    type_sep = u'\u001e'
    pathsep = os.sep + ("" if(os.altsep is None) else os.altsep)
    
    _rconn = None
    def _rconn_set_r_port(self, env, pref_name):
        # TODO: put this together with RConn.read_port_no'
        port = None
        if _xpcom_:
            koPrefs = components.classes["@activestate.com/koPrefService;1"] \
                .getService(components.interfaces.koIPrefService).prefs
            if koPrefs.hasPref(pref_name):
                try: port = int(koPrefs.getDouble(pref_name))
                except: port = int(koPrefs.getLong(pref_name))
                finally: port = None if port == 0 else port
        if port is None:
            try:
                # XXX: this does not work: how to access the preferences without xpcom?
                port = int(env.get_pref(pref_name))
            except:
                port = self._rconn.read_port_no()
        if port is not None:
            self._rconn.port = port
        pass
    
    def _eval_in_r(self, cmd, env):
        """evaluate cmd in R.
        """
        if self._rconn is None:
            self._rconn = RConn(-1)
            self._rconn_set_r_port(env, "sciviews.r.port")
            # env.add_pref_observer("sciviews.r.port", self._rconn_set_r_port)
        return self._rconn.eval_in_R(cmd)
    
    ##
    # Implicit triggering event, i.e. when typing in the editor.
    #
    def trg_from_pos(self, buf, pos, implicit=True, DEBUG=False, ac=None):
        """If the given position is a _likely_ trigger point, return a
        relevant Trigger instance. Otherwise return the None.
            "pos" is the position at which to check for a trigger point.
            "implicit" (optional) is a boolean indicating if this trigger
                is being implicitly checked (i.e. as a side-effect of
                typing). Defaults to true.
        """
        if pos < 3:
            return None

        acc = buf.accessor
        last_pos = pos - 1
        char = acc.char_at_pos(last_pos)
        
        # complete argument names after ,<space>
        complete_arg = (char in ' \t\n\r') and (acc.char_at_pos(last_pos - 1) == ',')
        if complete_arg:
            last_pos -= 1
            char = ','
        
        style = acc.style_at_pos(last_pos)
        
        if style == self.operator_style:
            if complete_arg or (char in '[('):
                infun = self._in_func(pos, acc)
                if infun is not None:
                    s, e, funcname, nargs, argnames, firstarg = infun
                    return Trigger(self.lang, TRG_FORM_CPLN, "args", pos, True,
                        funcname = funcname, firstarg = firstarg, nargs = nargs,
                        argnames = argnames)
                return None

            elif char in '@$:' and (char != ':' or \
                acc.char_at_pos(last_pos - 1) == ':'):
                vr = self._get_var_back(last_pos, acc)
                if vr is not None:
                    return Trigger(self.lang, TRG_FORM_CPLN, "variable", vr[4],
                        True, obj_name = ''.join(vr[2]), cutoff = vr[3])
        if style == self.string_style and char in self.pathsep:
            s, e, w = self._get_word_back(last_pos, acc)
            if len(w) < 2:
                return None
            return self._trg_complete_path(w, pos, buf.env)
        return None

    def _unquote(self, text, quotes = '`"\''):
        if(text[0] in quotes and text[-1] == text[0]):
            return text[1:len(text) - 1]
        return text

    def _is_bquoted(self, text):
        return len(text) > 1 and text.startswith('`') and text.endswith(text[0])


    ##
    # Explicit triggering event, i.e. Ctrl+J.
    #
    def preceding_trg_from_pos(self, buf, pos, curr_pos,
                               preceding_trg_terminators=None, DEBUG=False):

        if pos < 3:
            return None

        acc = buf.accessor
        last_pos = pos - 1

        style = acc.style_at_pos(last_pos)
        s, e, w = self._get_word_back(last_pos, acc)

        ch = acc.char_at_pos(pos)
        prv_ch = acc.char_at_pos(last_pos)
        # log.debug('w = "%s", ch = "%s", prv_ch = "%s", pos = %d, curr_pos = %d ' \
        #          % (w, ch, prv_ch, pos, curr_pos, ))
        if style in self.word_styles:
            if self._is_bquoted(w):
                return None
            s2, e2, w2 = self._get_word_back(s - 1, acc)
            # log.debug( 'w2 = "%s" ' % (w2, ) )

            if w2 and w2[-1] in ',(':
                infun = self._in_func(last_pos, acc)
                if infun is not None:
                    #print 'complete variable or argument "%s" for "%s"' % ( w, infun[2], )
                    s2, e2, funcname, nargs, argnames, firstarg = infun
                    return Trigger(self.lang, TRG_FORM_CPLN, "args", s, False,
                        funcname = funcname, firstarg = firstarg, nargs = nargs,
                        argnames = argnames, text = w)
                else:
                    return None
            else:
                vr = self._get_var_back(last_pos, acc)
                if vr is not None:
                    #print 'complete variable "%s"' % ( ''.join(vr[2]), )
                    return Trigger(self.lang, TRG_FORM_CPLN, "variable", vr[4],
                        False, obj_name = ''.join(vr[2]), cutoff = vr[3])
                return None
        if style == self.string_style:
            if len(w) < 2:
                return None
            return self._trg_complete_path(w, pos, buf.env)

        if not w:
            return None
        if w[-1] in ',(':
            infun = self._in_func(pos, acc)
            if infun is not None:
                s2, e2, funcname, nargs, argnames, firstarg = infun
                print 'arguments for "%s"' % ( infun[2], )
                return Trigger(self.lang, TRG_FORM_CPLN, "args", \
                    pos, False, funcname = funcname, firstarg = firstarg, \
                    nargs = nargs, argnames = argnames)

        elif w[-1] in '@$:':
            vr = self._get_var_back(last_pos, acc)
            if vr is not None:
                v = ''.join(vr[2])
                print 'complete "%s"' % ( v, )
                return Trigger(self.lang, TRG_FORM_CPLN, "variable", vr[4], ### pos + 1
                    False, obj_name = v, cutoff = vr[3])
        elif w in ('[', '[['):
            infun = self._in_func(pos, acc)
            if infun is not None:
                # log.debug( 'w = "%s", in_func = "%s" ' % (w, str(infun), ) )
                s2, e2, funcname, nargs, argnames, firstarg = infun
                # log.debug('arguments for "%s"' % ( infun[2], ))
                return Trigger(self.lang, TRG_FORM_CPLN, "args", \
                    pos, False, funcname = funcname, firstarg = firstarg, \
                    nargs = nargs, argnames = argnames)
            else:
                pass
                # log.debug( 'w = "%s", in_func is None' % (w, ) )
        # log.debug( 'None? w = "%s" ' % (w, ) )
        return None

    def async_eval_at_trg(self, buf, trg, ctlr):
        if _xpcom_:
            trg = UnwrapObject(trg)
            ctlr = UnwrapObject(ctlr)
        pos = trg.pos
        ctlr.start(buf, trg)
        extra = trg.extra

        if trg.id == (self.lang, TRG_FORM_CPLN, "args") or \
            trg.id == (self.lang, TRG_FORM_CPLN, "variable-or-args") :
            completions = self._get_completions_args(
                buf.env,
                extra.get('funcname'), extra.get('firstarg'), extra.get('nargs'),
                extra.get('argnames'), extra.get("text"))
        elif trg.id == (self.lang, TRG_FORM_CPLN, "variable") or \
            trg.id == (self.lang, TRG_FORM_CPLN, "sub-items") :
            completions = self._get_completions_default(
                extra.get('obj_name'), extra.get('cutoff'), buf.env)
        elif trg.id == (self.lang, TRG_FORM_CPLN, "path"):
            completions = self._get_completions_path(extra.get('text'))
        else:
            ctlr.error("Unknown trigger type: %r" % (trg, ))
            ctlr.done("error")
            return

        if completions is None:
            ctlr.done("not found")
            return

        result, cplns = completions
        if result == "error":
            ctlr.error("Nothing found" if completions is None else cplns)
            ctlr.done("error")
            return
        if result == "success":
            cplns = [x for x in cplns if len(x) == 2]
            cplns.sort(key = lambda x: x[1].lower() )
            ctlr.set_cplns(cplns)
            ctlr.done(cplns)
            return
            #ctlr.info("Not found for %r" % (trg, ))
            #ctlr.done("none found")
            #return

    #
    #   Rules for implementation:
    #- Must call ctlr.start(buf, trg) at start.
    #- Should call ctlr.set_desc(desc) near the start to provide a
    #  short description of the evaluation.
    #- Should log eval errors via ctlr.error(msg, args...).
    #- Should log other events via ctlr.{debug|info|warn}.
    #- Should respond to ctlr.abort() in a timely manner.
    #- If successful, must report results via one of
    #  ctlr.set_cplns() or ctlr.set_calltips().
    #- Must call ctlr.done(some_reason_string) when done.

    def _trg_complete_path(self, w, pos, env):
        path = w.lstrip('\'\"')
        abspath = os.path.expanduser(path)
        isabs = os.path.isabs(abspath)
        #posoff = 1 if all([ path.find(x) == -1 for x in self.pathsep ]) else 1
        # append /
        tokenlen = len(os.path.basename(abspath))
        abspath = os.path.dirname(abspath)
        if not abspath or abspath[-1] not in self.pathsep:
            abspath += os.sep
        if not isabs:
            pwd = self._rconn.eval_in_R('cat(getwd())').strip()
            if os.path.exists(pwd):
                abspath = os.path.join(pwd, abspath)
        # log.debug("complete path: " + abspath);
        if os.path.exists(abspath):
            # log.debug("Complete abs path: " + abspath)
            return Trigger(self.lang, TRG_FORM_CPLN, "path",
                pos - tokenlen, False, text = abspath)
        return None

    def _get_completions_args(self, env, fname, frstarg, nargs, argnames = None,
            text = ''):
        fname = self._unquote(fname)
        # log.debug("fname = '%s'" % (fname, ) )

        if fname in ('library', 'require', 'base::library', 'base::require') \
            and nargs == 1:
            cmd = 'sv_completeSpecial("library")'
        elif fname in ('detach', 'base::detach') and nargs == 1:
            cmd = 'sv_completeSpecial("search")'
        elif fname in ('data', 'base::data') and nargs == 1:
            cmd = 'sv_completeSpecial("data"); sv_completeArgs("data")'
        elif fname == 'par':
            cmd = 'sv_completeSpecial("par"); sv_completeArgs("par")'
        elif fname in self.func_graphics:
            cmd = ('sv_completeArgs("%s", %s); sv_completeSpecial("par")' \
                  % (fname, frstarg, )) \
                  + ('; sv_completion("%s")' % (text, ) if text else '')
        elif fname in ('options', 'getOption'):
            cmd = 'sv_completeSpecial("options"); sv_completeArgs("%s")' \
                  % (fname, )
        elif fname in ('[', '[['):
            cmd = 'sv_completeArgs("%s", %s);' % (fname, frstarg, )
            #if fname == '[[' or nargs == 2:
            cmd += 'sv_completeSpecial("[", %s, argpos=%d)' % (frstarg, nargs, )
        else:
            cmd = ('sv_completeArgs("%s", %s);' % (fname, frstarg, )) \
                + ('sv_completion("%s")' % (text, ) if text else '')
        #ret = []
        res = self._eval_in_r(cmd, env).rstrip()
        if len(res) == 0:
            return ('none found', 'No completions found')

        if res.startswith(u'\x03'):
            return ('error', res.strip("\x02\x03\r\n"))
        
        try:
            ret = [ tuple(x.split(self.type_sep)) for x in res.split(os.linesep) ]
        except:
            return ('none found', 'No completions available')
        if argnames:
            #log.debug("argnames = %s" % (", ".join(argnames) , ))
            argnames = [ x + ' =' for x in argnames ]
            ret = [ x for x in ret if x[1] not in argnames ]
            if not len(ret):
                return ('none found', 'No completions found')
        return ('success', ret, )

    def _get_completions_path(self, text):
        try:
            res = os.listdir(text)
            # log.debug("Complete abs path: %d" % (len(res), ))
            return ('success', [ \
                ("directory" if os.path.isdir(text + os.sep + x) else \
                 "file", x, ) for x in res ] )
            #return ('success', [('directory', x) for x in res ], )
        except:
            return ('none found', None, )

    def _get_completions_default(self, text, cutoff, env):
        if not text.strip(): return None
        cmd = 'sv_completion("%s")' % text.replace('"', '\\"')
        res = self._eval_in_r(cmd, env)
        
        #TODO: on timeout an empty string is returned
        #u'\x03Error: could not find function "completion"\r\n\x02'
        if res.startswith(u'\x03'):
            return ('error', res.strip("\x02\x03\r\n"))
        cplstr = res.replace('\x03', '').replace('\x02', '')
        if not cplstr: return None
        
        try:
            cpl = [ ( x[0], x[1][cutoff:] )
                for x in [ tuple(x.split(self.type_sep)) for x in
                          res.split(os.linesep) ] if len(x[1]) > cutoff ]
        except:
            return ('none found', 'No completions available')
            
        if len(cpl):
            return ('success', cpl)

        return ('none found', 'No completions found')

    def _skip_back_ws(self, pos, acc):
        if acc.style_at_pos(pos) == self.whitespace_style:
            return acc.contiguous_style_range_from_pos(pos)[0] - 1
        return pos

    def _get_word_back(self, pos, acc):
        pos = self._skip_back_ws(pos, acc)
        if pos < 0:
            return (0, 0, '')
        s, e = acc.contiguous_style_range_from_pos(pos)
        e = min(pos + 1, e)
        return (s, e, acc.text_range(s, e))

    def _get_var_back(self, pos, acc):
        if pos < 2 or pos >= acc.length:
            return None
        token = []
        # variable [$@]? <|>
        s, e0, w = self._get_word_back(pos, acc)
        print 'w = %r' % (w, )
        style = acc.style_at_pos(s)
        if style in self.word_styles:
            token += [ w ]
            s0 = s
            cutoff = e0 - s
            trg_pos = s
            if s > 1:
                s, e, w = self._get_word_back(s - 1, acc)
        elif style == self.operator_style:
            cutoff = 0
            trg_pos = pos + 1

        while s > 1:
            if w in '$@':
                s, e, w2 = self._get_word_back(s - 1, acc)
                print 'w = %r, w2 = %r' % (w, w2, )
                style = acc.style_at_pos(s)
                if style in self.word_styles:
                    token += [ w, w2 ]
                    s0 = s
                else:
                    break
            else:
                break
            s, e, w = self._get_word_back(s - 1, acc)
            if not (style == self.element_style or self._is_bquoted(w2)):
                break

        if w in ('::', ':::') and s > 1:
            s, e, w2 = self._get_word_back(s - 1, acc)
            style = acc.style_at_pos(s)
            if style == self.namespace_style:
                token += [ w, w2 ]
                s0 = s
            else:
                return None
        elif not style in self.varname_styles:
            return None
        print s, s0

        token.reverse()
        return (s0, e0, token, reduce(lambda v, x: v + len(x), token, 0) - cutoff,
                trg_pos)
    
    def _brace_match(self, position, acc):
        """ 'SilverCityAccessor' does not have 'scimoz' element which provides
        'braceMatch', so we need to implement it. This function is based on the
        original c++ version in scintilla.
        """
        if position < 0 or position >= acc.length():
            return -1
        if hasattr(acc, "scimoz"):
            return acc.scimoz.braceMatch(position)
       
        def _next_position(position, direction, style, acc):
            if acc.style_at_pos(position) == style:
               return position + direction
            csr = acc.contiguous_style_range_from_pos(position)
            if(direction == -1):
                return csr[0] - 1
            return csr[1]
        
        def _brace_opposite(s):
            brace = '([{)]}'
            oppos = ')]}([{'
            pos = brace.find(s)
            if pos != -1:
                return oppos[pos]
            return '\0'
        
        chBrace = acc.char_at_pos(position)
        chSeek = _brace_opposite(chBrace)
        if chSeek == '\0':
            return -1
        styBrace = acc.style_at_pos(position)
        length = acc.length()
    
        direction = 1 if chBrace in '[{(' else -1
        depth = 1
        position = _next_position(position, direction, styBrace, acc)
        if hasattr(acc, 'tokens'):
            endStyled = acc.tokens[-1L]['end_index']
        else:
            endStyled = acc.length()
     
        while (position >= 0) and (position < length) :
            print 'position = %d' % position
            chAtPos = acc.char_at_pos(position)
            styAtPos = acc.style_at_pos(position)
            if (position > endStyled) or (styAtPos == styBrace):
                if chAtPos == chBrace:
                    depth += 1
                if chAtPos == chSeek:
                    depth -= 1
                if depth == 0:
                    print "end position = %d" % position
                    return position
            positionBeforeMove = position
            position = _next_position(position, direction, styBrace, acc)
            if position == positionBeforeMove:
                break
        return -1

    def _in_func(self, pos, acc):
        p = pos - 1
        p_min = max(0, pos - 512) #  whole function call in R can be very long
                                  #  e.g. with 'lapply'
        
        arg_count = 1
        argnames = list()
        commapos = -1
        while p > p_min:
            ch = acc.char_at_pos(p)
            if acc.style_at_pos(p) == self.string_style:
               p = acc.contiguous_style_range_from_pos(p)[0] - 1
            elif ch in ")]}":
                p = self._brace_match(p, acc) - 1
            #elif ch in "[{":
            elif ch == "{":
                return None
                # TODO: 'name[' --> '`[`(name'

            elif ch == "[":
                # function name:
                var = self._get_var_back(p, acc)
                if(var is not None):
                    fn_start, fn_end, fn_word, x_, x_ = var
                    fn_word = ''.join(fn_word)
                else:
                    return None
                #fn_start, fn_end, fn_word = self._get_word_back(p, acc)
                print "word = '%s', start = %d" % (fn_word, fn_start, )
                # _get_var_back ===> (s, e0, token, cutoff, trg_pos)
                var = self._get_var_back(fn_start - 1, acc)
                print "_get_var_back(%d) = '%s'" % (fn_start - 1, var, )
                if var is not None:
                    start, end, word, x_, x_ = self._get_var_back(fn_start - 1, acc)
                    #start, end, word = self._get_word_back(fn_start - 1, acc)
                    if acc.style_at_pos(start) in self.word_styles:
                        argnames.reverse()
                        return (fn_start, p, fn_word, arg_count, argnames, ''.join(word) )
                return None

            elif ch == "(":
                # function name:
                var = self._get_var_back(p - 1, acc)
                if(var is not None):
                    fn_start, fn_end, fn_word, x_, x_ = var
                    fn_word = ''.join(fn_word)
                else:
                    return None
                #fn_start, fn_end, fn_word = self._get_word_back(p - 1, acc)
                if acc.style_at_pos(fn_start) in self.word_styles:
                    # namespace ::[:] function:
                    #print "fn_start = ", fn_start
                    if fn_start > 1:
                        start, end, op_word = self._get_word_back(fn_start - 1, acc)
                        if op_word in ('::', ':::'):
                            start, end, ns_word = self._get_word_back(start - 1, acc)
                            if acc.style_at_pos(start) == self.namespace_style:
                                fn_word = ns_word + op_word + fn_word
                                fn_start = start
                    argnames.reverse()
                    return (fn_start, p, fn_word, arg_count, argnames,
                                acc.text_range(p + 1, commapos).strip()
                                    if commapos > 0 else None)
                break
            else:
                if ch == ',':
                    commapos = p
                    arg_count += 1
                elif ch == '=':
                    start, end, word = self._get_word_back(p - 1, acc)
                    if acc.style_at_pos(start) == self.argument_style:
                        argnames += [ word ]
                p1 = self._skip_back_ws(p, acc);
                if p1 == p:
                    p -= 1
                else:
                    p = p1
        return None