Sublime Text 3插件:带有Edit对象的ValueError?

时间:2014-07-19 21:11:22

标签: python sublimetext sublimetext3 sublime-text-plugin

我正在构建一个Sublime Text 3插件,以使用goo.gl API缩短网址。请记住,以下代码是从其他插件和教程代码一起被黑客攻击的。我对Python有没有经验

插件确实实际上是按原样工作的。 URL缩短并替换为内联。这是插件代码:

import sublime
import sublime_plugin
import urllib.request
import urllib.error
import json
import threading


class ShortenUrlCommand(sublime_plugin.TextCommand):

    def run(self, edit):
        sels = self.view.sel()

        threads = []
        for sel in sels:
            url = self.view.substr(sel)
            thread = GooglApiCall(sel, url, 5)  # Send the selection, the URL and timeout to the class
            threads.append(thread)
            thread.start()

        # Wait for threads
        for thread in threads:
            thread.join()

        self.view.sel().clear()
        self.handle_threads(edit, threads, sels)

    def handle_threads(self, edit, threads, sels, offset=0, i=0, dir=1):
        next_threads = []
        for thread in threads:
            sel = thread.sel
            result = thread.result
            if thread.is_alive():
                next_threads.append(thread)
                continue
            if thread.result == False:
                continue
            offset = self.replace(edit, thread, sels, offset)
        thread = next_threads

        if len(threads):
            before = i % 8
            after = (7) - before
            if not after:
                dir = -1
            if not before:
                dir = 1
            i += dir
            self.view.set_status("shorten_url", "[%s=%s]" % (" " * before, " " * after))
            sublime.set_timeout(lambda: self.handle_threads(edit, threads, sels, offset, i, dir), 100)
            return

        self.view.erase_status("shorten_url")
        selections = len(self.view.sel())
        sublime.status_message("URL shortener successfully ran on %s URL%s" %
            (selections, "" if selections == 1 else "s"))

    def replace(self, edit, thread, sels, offset):
        sel = thread.sel
        result = thread.result
        if offset:
            sel = sublime.Region(edit, thread.sel.begin() + offset, thread.sel.end() + offset)
        self.view.replace(edit, sel, result)
        return


class GooglApiCall(threading.Thread):
    def __init__(self, sel, url, timeout):
        self.sel = sel
        self.url = url
        self.timeout = timeout
        self.result = None
        threading.Thread.__init__(self)

    def run(self):
        try:
            apiKey = "xxxxxxxxxxxxxxxxxxxxxxxx"
            requestUrl = "https://www.googleapis.com/urlshortener/v1/url"
            data = json.dumps({"longUrl": self.url})
            binary_data = data.encode("utf-8")
            headers = {
                "User-Agent": "Sublime URL Shortener",
                "Content-Type": "application/json"
            }
            request = urllib.request.Request(requestUrl, binary_data, headers)
            response = urllib.request.urlopen(request, timeout=self.timeout)
            self.result = json.loads(response.read().decode())
            self.result = self.result["id"]
            return

        except (urllib.error.HTTPError) as e:
            err = "%s: HTTP error %s contacting API. %s." % (__name__, str(e.code), str(e.reason))
        except (urllib.error.URLError) as e:
            err = "%s: URL error %s contacting API" % (__name__, str(e.reason))

        sublime.error_message(err)
        self.result = False


问题是每次插件运行时我都会在控制台中收到以下错误:

Traceback (most recent call last):
  File "/Users/joejoinerr/Library/Application Support/Sublime Text 3/Packages/URL Shortener/url_shortener.py", line 51, in <lambda>
    sublime.set_timeout(lambda: self.handle_threads(edit, threads, sels, offset, i, dir), 100)
  File "/Users/joejoinerr/Library/Application Support/Sublime Text 3/Packages/URL Shortener/url_shortener.py", line 39, in handle_threads
    offset = self.replace(edit, thread, sels, offset)
  File "/Users/joejoinerr/Library/Application Support/Sublime Text 3/Packages/URL Shortener/url_shortener.py", line 64, in replace
    self.view.replace(edit, sel, result)
  File "/Applications/Sublime Text.app/Contents/MacOS/sublime.py", line 657, in replace
    raise ValueError("Edit objects may not be used after the TextCommand's run method has returned")
ValueError: Edit objects may not be used after the TextCommand's run method has returned


我不确定该错误是什么问题。我做了一些研究,我理解解决方案可以在this question的答案中进行,但由于我缺乏Python知识,我无法弄清楚如何使其适应我的用例。

1 个答案:

答案 0 :(得分:2)

我正在为Sublime搜索Python自动完成插件并找到了这个问题。我喜欢你的插件想法。你有没有得到它的工作? ValueError告诉您在edit返回后,您尝试将ShortenUrlCommand.run参数用于ShortenUrlCommand.run。我认为您可以使用begin_editend_edit在Sublime Text 2中执行此操作,但在3中,您的插件必须在run返回(https://www.sublimetext.com/docs/3/porting_guide.html)之前完成所有编辑。

在您的代码中,handle_threads函数每100毫秒检查GoogleApiCall个线程,并对已完成的任何线程执行替换。但是handle_threads有一个错字导致它永远运行:thread = next_threads它应该是threads = next_threads。这意味着永远不会从活动线程列表中删除已完成的线程,并且在handle_threads的每次调用中都会处理所有线程(最终抛出您看到的异常)。

您实际上并不需要担心GoogleApiCall踏板是否已在handle_threads中完成,因为您在调用join之前在每个踏板上调用handle_threads (有关threadinghttps://docs.python.org/2/library/threading.html)的详细信息,请参阅python join文档。您知道线程已完成,因此您可以执行以下操作:

def handle_threads(self, edit, threads, sels):
    offset = 0
    for thread in threads:
        if thread.result:
            offset = self.replace(edit, thread, sels, offset)
    selections = len(threads)
    sublime.status_message("URL shortener successfully ran on %s URL%s" %
        (selections, "" if selections == 1 else "s"))

这仍有问题:它无法正确处理多个选择,并且它会阻止Sublime中的UI线程。

多项选择

当您替换多个选择时,您必须考虑替换文本的长度可能与它替换的文本的长度不同。这会将文本移到后面,您必须调整后续选定区域的索引。例如,假设在以下文本中选择了URL,并且您要用缩短的URL替换它们:

          1         2         3         4         5         6         7   
01234567890123456789012345678901234567890123456789012345678901234567890123
blah blah http://example.com/long blah blah http://example.com/longer blah

第二个URL占用索引44到68.替换第一个URL后,我们有:

          1         2         3         4         5         6         7
01234567890123456789012345678901234567890123456789012345678901234567890123
blah blah http://goo.gl/abc blah blah http://example.com/longer blah

现在第二个URL占用了索引38到62.它被移动了-6:我们刚刚替换的字符串长度与我们替换它的字符串长度之间的差异。您需要跟踪这些差异,并在每次更换后更新它。看起来您已经考虑过offset这个问题了,但是从来没有实现它。

def handle_threads(self, edit, threads, sels):
    offset = 0
    for thread in threads:
        if thread.result:
            offset = self.replace(edit, thread.sel, thread.result, offset)
    selections = len(threads)
    sublime.status_message("URL shortener successfully ran on %s URL%s" %
        (selections, "" if selections == 1 else "s"))

def replace(self, edit, selection, replacement_text, offset):
    # Adjust the selection region to account for previous replacements
    adjusted_selection = sublime.Region(selection.begin() + offset,
                                        selection.end() + offset)
    self.view.replace(edit, adjusted_selection, replacement_text)

    # Update the offset for the next replacement
    old_len = selection.size()
    new_len = len(replacement_text)
    delta = new_len - old_len
    new_offset = offset + delta
    return new_offset

阻止UI线程

我不熟悉Sublime插件,因此我查看了如何在Gist插件中处理它(https://github.com/condemil/Gist)。它们在HTTP请求期间阻止UI线程。这似乎是不可取的,但我认为如果您不阻止可能会出现问题:用户可能会在插件完成更新之前更改文本缓冲区并使选择索引无效。如果您想沿着这条路前进,可以尝试将网址缩短调用移至WindowCommand。然后,一旦有了替换文本,就可以在每个当前视图上执行替换命令。此示例获取当前视图并在其上执行ShortenUrlCommand。您必须将收集缩短网址的代码移至ShortenUrlWrapperCommand.run

class ShortenUrlWrapperCommand(sublime_plugin.WindowCommand):
    def run(self):
        view = self.window.active_view()
        view.run_command("shorten_url")