[SVN] BUG: Output Text not inserted at caret when input is Scope

Alain O'Dea alain.odea at gmail.com
Tue Feb 19 02:08:49 UTC 2008


On 18-Feb-08, at 5:30 PM, Allan Odgaard wrote:

> On 18 Feb 2008, at 02:23, Alain O'Dea wrote:
>
>> [...]
>> I can't get this to work. I am left with text appearing after the  
>> end of the scope. For clarification the scope I am working with can  
>> be many lines. How do I make TextMate insert the output at the  
>> caret instead of at the end of the selection/scope?
>
> Are you using “selection or scope” as input and “insert as snippet”  
> as output?
>
> You will sometimes get more input that what would be ideal (using a  
> scope selector), but there is presently no way around that.
>
> The Objective-C bundle has completion commands that takes current  
> scope as input, you may want to look at the se for examples. If you  
> still cannot get it to work, please post what you are actually doing.


I am building a code completion system for Erlang in TextMate. The  
process is quite involved, but it gets completions extremely quickly.

command_server (command_server.erl) receives a command name followed  
by input lines over TCP. If "vars" is sent then command_server invokes  
vars:exec/2 (vars.erl) which  parses the input and returns the  
variables in it. command_server then invokes tm_menu:get_selection/1  
(tm_menu.erl) which builds a plist from the variables lis, invokes  
tm_dialog, and returns the user's selection once made. command_server  
then returns the result to the client over TCP. In theory invoking  
Variable Completion (Command) should insert the selected variable at  
the caret. Instead it inserts at the end of the scope.

Realizing that TextMate was going to insert at the end of the input I  
decided to hack around it using Macros. To this end command_server,  
just before responding to the to TCP client, invokes clipboard:copy/1  
which uses pbcopy to add the result to the clipboard. As a result I  
can invoke Variable Completion into Clipboard (Command) and then Edit- 
 >Paste to drop the result right at the caret. Unfortunately there  
seems to be a race condition on the clipboard and this macro often  
drops the previous clipboard contents. This occurs even if the local  
clipboard option for the Macro is turned off. If I paste again  
manually the correct value is pasted. Using the Paste (Command)  
instead of Edit->Paste in the macro works correctly, but with a  
noticeable lag between selecting the completion and the subsequent  
insertion.

Listings:
=========

erl_vars.py:
#!/usr/bin/env python
import socket
import sys
import os
import time
module = os.getenv('TM_FILEPATH').split('/').pop().split('.').pop(0)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1',2345))
s.send('vars\n')
for line in sys.stdin:
     s.send(line.rstrip() + os.linesep)
s.send('eof' + os.linesep)
# rendezvous with Erlang server
print s.recv(4096)

Variable Completion (Command):
python erl_vars.py
-input: Selected Text or Scope
-output: Insert as Text

Variable Completion into Clipboard (Command):
python erl_vars.py
-input: Selected Text or Scope
-output: None

Paste (Command):
pbpaste
-input: None
-output: Insert as Text

Paste as Snippet (Command):
pbpaste
-input: None
-output: Insert as Snippet

command_server.erl:
-module(command_server).
-export([start/0]).

start() ->
     {ok, Listen} = gen_tcp:listen(2345, [list, {packet, line},
                                          {reuseaddr, true},
                                          {active, true}]),
     spawn(fun() -> par_connect(Listen) end).

par_connect(Listen) ->
     {ok, Socket} = gen_tcp:accept(Listen),
     io:format("Client session started~n"),
     spawn(fun() -> par_connect(Listen) end),
     loop(Socket).

loop(Socket) ->
     receive
         {tcp, Socket, ModuleRaw} ->
             Module = list_to_atom(trim(ModuleRaw)),
             Reply = Module:exec(tm_command, iter(Socket)),
             clipboard:copy(Reply),
             gen_tcp:send(Socket, list_to_binary(Reply)),
             loop(Socket);
         {tcp_closed, Socket} ->
             io:format("Client session ended~n")
     end.

iter(Socket) ->
     fun() ->
         receive
             {tcp, Socket, "eof\n"} ->
                 eof;
             {tcp, Socket, Line} ->
                 trim(Line);
             {tcp_closed, Socket} ->
                 eof
         end
     end.

trim(String) ->
     {ok, Trimmed, _} = regexp:sub(String, "[\r\n]", ""),
     string:strip(Trimmed).

clipboard.erl:
-module(clipboard).
-export([copy/1]).

copy(Data) ->
     Clipboard = open_port({spawn, "pbcopy"}, [stream]),
     Clipboard ! {self(), {command, Data}},
     Clipboard ! {self(), close},
     receive
         {Clipboard, closed} -> true
     end.

tm_menu.erl:
-module(tm_menu).
-export([get_selection/1]).
-include_lib("xmerl/include/xmerl.hrl").

get_selection(MenuItems) ->
     %% TODO: need better way to get path for tm_dialog
     PlistXml = os:cmd(io_lib:format("/Applications/TextMate.app/ 
Contents/PlugIns/Dialog.tmplugin/Contents/Resources/tm_dialog -u -p  
\"~s\"", [ascii_plist(MenuItems)])),
     {PlistDoc, _Rest} = xmerl_scan:string(PlistXml),
     [#xmlText{value=Selected}] = xmerl_xpath:string("/plist/dict/ 
dict[preceding-sibling::key[1]='selectedMenuItem']/string[preceding- 
sibling::key[1]='title']/text()", PlistDoc),
     Selected.

ascii_plist(MenuItems) ->
     lists:flatten(
         lists:reverse([");}"|
             lists:foldl(
                 fun(Item, Completions) ->
                     [io_lib:format("{title = ~s;},", [Item])| 
Completions]
                 end,
                 ["{menuItems = ("],
                 MenuItems
             )
         ])
     ).

vars.erl:
-module(vars).
-export([exec/2]).

exec(tm_command, Iter) ->
     tm_menu:get_selection(list(Iter)).

list(Iter) ->
     sets:to_list(
         sets:from_list(
             list(Iter, Iter()))).

list(_, eof) ->
     [];
list(Iter, Line) ->
     matches(Line, list(Iter, Iter())).

matches(Line, MatchesIn) ->
     {match, MatchIndices} = regexp:matches(Line, "[A-Z][a-zA- 
Z0-9_@]*"),
     lists:foldl(
         fun({Start, Length}, Matches) ->
             [string:substr(Line, Start, Length)|Matches]
         end,
         MatchesIn, MatchIndices).




More information about the textmate-dev mailing list