To: vim_dev@googlegroups.com Subject: Patch 7.4.1231 Fcc: outbox From: Bram Moolenaar Mime-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ------------ Patch 7.4.1231 Problem: JSON messages are not parsed properly. Solution: Queue received messages. Files: src/eval,c src/channel.c, src/json.c, src/proto/eval.pro, src/proto/channel.pro, src/proto/json.pro, src/structs.h *** ../vim-7.4.1230/src/channel.c 2016-01-31 20:24:09.962066926 +0100 --- src/channel.c 2016-02-01 02:02:48.158391509 +0100 *************** *** 74,85 **** struct readqueue *next; struct readqueue *prev; }; ! typedef struct readqueue queue_T; typedef struct { sock_T ch_fd; /* the socket, -1 for a closed channel */ int ch_idx; /* used by channel_poll_setup() */ ! queue_T ch_head; /* dummy node, header for circular queue */ int ch_error; /* When TRUE an error was reported. Avoids giving * pages full of error messages when the other side --- 74,93 ---- struct readqueue *next; struct readqueue *prev; }; ! typedef struct readqueue readq_T; ! ! struct jsonqueue ! { ! typval_T *value; ! struct jsonqueue *next; ! struct jsonqueue *prev; ! }; ! typedef struct jsonqueue jsonq_T; typedef struct { sock_T ch_fd; /* the socket, -1 for a closed channel */ int ch_idx; /* used by channel_poll_setup() */ ! readq_T ch_head; /* dummy node, header for circular queue */ int ch_error; /* When TRUE an error was reported. Avoids giving * pages full of error messages when the other side *************** *** 100,106 **** char_u *ch_callback; /* function to call when a msg is not handled */ char_u *ch_req_callback; /* function to call for current request */ ! int ch_json_mode; } channel_T; /* --- 108,115 ---- char_u *ch_callback; /* function to call when a msg is not handled */ char_u *ch_req_callback; /* function to call for current request */ ! int ch_json_mode; /* TRUE for a json channel */ ! jsonq_T ch_json_head; /* dummy node, header for circular queue */ } channel_T; /* *************** *** 125,130 **** --- 134,140 ---- { int idx; channel_T *new_channels; + channel_T *ch; if (channels != NULL) for (idx = 0; idx < channel_count; ++idx) *************** *** 139,156 **** if (channels != NULL) mch_memmove(new_channels, channels, sizeof(channel_T) * channel_count); channels = new_channels; ! (void)vim_memset(&channels[channel_count], 0, sizeof(channel_T)); ! channels[channel_count].ch_fd = (sock_T)-1; #ifdef FEAT_GUI_X11 ! channels[channel_count].ch_inputHandler = (XtInputId)NULL; #endif #ifdef FEAT_GUI_GTK ! channels[channel_count].ch_inputHandler = 0; #endif #ifdef FEAT_GUI_W32 ! channels[channel_count].ch_inputHandler = -1; #endif return channel_count++; } --- 149,172 ---- if (channels != NULL) mch_memmove(new_channels, channels, sizeof(channel_T) * channel_count); channels = new_channels; ! ch = &channels[channel_count]; ! (void)vim_memset(ch, 0, sizeof(channel_T)); ! ch->ch_fd = (sock_T)-1; #ifdef FEAT_GUI_X11 ! ch->ch_inputHandler = (XtInputId)NULL; #endif #ifdef FEAT_GUI_GTK ! ch->ch_inputHandler = 0; #endif #ifdef FEAT_GUI_W32 ! ch->ch_inputHandler = -1; #endif + /* initialize circular queues */ + ch->ch_head.next = &ch->ch_head; + ch->ch_head.prev = &ch->ch_head; + ch->ch_json_head.next = &ch->ch_json_head; + ch->ch_json_head.prev = &ch->ch_json_head; return channel_count++; } *************** *** 412,492 **** void channel_set_req_callback(int idx, char_u *callback) { vim_free(channels[idx].ch_req_callback); channels[idx].ch_req_callback = callback == NULL ? NULL : vim_strsave(callback); } /* ! * Decode JSON "msg", which must have the form "[expr1, expr2, expr3]". ! * Put "expr1" in "tv1". ! * Put "expr2" in "tv2". ! * Put "expr3" in "tv3". If "tv3" is NULL there is no "expr3". ! * ! * Return OK or FAIL. */ int ! channel_decode_json(char_u *msg, typval_T *tv1, typval_T *tv2, typval_T *tv3) { js_read_T reader; typval_T listtv; ! reader.js_buf = msg; reader.js_eof = TRUE; reader.js_used = 0; ! json_decode(&reader, &listtv); ! ! if (listtv.v_type == VAR_LIST) ! { ! list_T *list = listtv.vval.v_list; ! ! if (list->lv_len == 2 || (tv3 != NULL && list->lv_len == 3)) { ! /* Move the item from the list and then change the type to avoid the ! * item being freed. */ ! *tv1 = list->lv_first->li_tv; ! list->lv_first->li_tv.v_type = VAR_NUMBER; ! *tv2 = list->lv_first->li_next->li_tv; ! list->lv_first->li_next->li_tv.v_type = VAR_NUMBER; ! if (tv3 != NULL) { ! if (list->lv_len == 3) ! { ! *tv3 = list->lv_last->li_tv; ! list->lv_last->li_tv.v_type = VAR_NUMBER; ! } ! else ! tv3->v_type = VAR_UNKNOWN; } - list_unref(list); - return OK; } } - - /* give error message? */ - clear_tv(&listtv); - return FAIL; } /* ! * Invoke the "callback" on channel "idx". */ static void ! invoke_callback(int idx, char_u *callback, typval_T *argv) { ! typval_T rettv; ! int dummy; ! argv[0].v_type = VAR_NUMBER; ! argv[0].vval.v_number = idx; ! call_func(callback, (int)STRLEN(callback), ! &rettv, 2, argv, 0L, 0L, &dummy, TRUE, NULL); ! /* If an echo command was used the cursor needs to be put back where ! * it belongs. */ ! setcursor(); ! cursor_on(); ! out_flush(); } /* --- 428,615 ---- void channel_set_req_callback(int idx, char_u *callback) { + /* TODO: make a list of callbacks */ vim_free(channels[idx].ch_req_callback); channels[idx].ch_req_callback = callback == NULL ? NULL : vim_strsave(callback); } /* ! * Invoke the "callback" on channel "idx". ! */ ! static void ! invoke_callback(int idx, char_u *callback, typval_T *argv) ! { ! typval_T rettv; ! int dummy; ! ! argv[0].v_type = VAR_NUMBER; ! argv[0].vval.v_number = idx; ! ! call_func(callback, (int)STRLEN(callback), ! &rettv, 2, argv, 0L, 0L, &dummy, TRUE, NULL); ! /* If an echo command was used the cursor needs to be put back where ! * it belongs. */ ! setcursor(); ! cursor_on(); ! out_flush(); ! } ! ! /* ! * Return the first buffer from the channel and remove it. ! * The caller must free it. ! * Returns NULL if there is nothing. ! */ ! char_u * ! channel_get(int idx) ! { ! readq_T *head = &channels[idx].ch_head; ! readq_T *node; ! char_u *p; ! ! if (head->next == head || head->next == NULL) ! return NULL; ! node = head->next; ! /* dispose of the node but keep the buffer */ ! p = node->buffer; ! head->next = node->next; ! node->next->prev = node->prev; ! vim_free(node); ! return p; ! } ! ! /* ! * Returns the whole buffer contents concatenated. ! */ ! static char_u * ! channel_get_all(int idx) ! { ! /* Concatenate everything into one buffer. ! * TODO: avoid multiple allocations. */ ! while (channel_collapse(idx) == OK) ! ; ! return channel_get(idx); ! } ! ! /* ! * Collapses the first and second buffer in the channel "idx". ! * Returns FAIL if that is not possible. */ int ! channel_collapse(int idx) ! { ! readq_T *head = &channels[idx].ch_head; ! readq_T *node = head->next; ! char_u *p; ! ! if (node == head || node == NULL || node->next == head) ! return FAIL; ! ! p = alloc((unsigned)(STRLEN(node->buffer) ! + STRLEN(node->next->buffer) + 1)); ! if (p == NULL) ! return FAIL; /* out of memory */ ! STRCPY(p, node->buffer); ! STRCAT(p, node->next->buffer); ! vim_free(node->next->buffer); ! node->next->buffer = p; ! ! /* dispose of the node and buffer */ ! head->next = node->next; ! node->next->prev = node->prev; ! vim_free(node->buffer); ! vim_free(node); ! return OK; ! } ! ! /* ! * Use the read buffer of channel "ch_idx" and parse JSON messages that are ! * complete. The messages are added to the queue. ! */ ! void ! channel_read_json(int ch_idx) { js_read_T reader; typval_T listtv; + jsonq_T *item; + jsonq_T *head = &channels[ch_idx].ch_json_head; ! if (channel_peek(ch_idx) == NULL) ! return; ! ! /* TODO: make reader work properly */ ! /* reader.js_buf = channel_peek(ch_idx); */ ! reader.js_buf = channel_get_all(ch_idx); reader.js_eof = TRUE; + /* reader.js_eof = FALSE; */ reader.js_used = 0; ! /* reader.js_fill = channel_fill; */ ! reader.js_cookie = &ch_idx; ! if (json_decode(&reader, &listtv) == OK) ! { ! item = (jsonq_T *)alloc((unsigned)sizeof(jsonq_T)); ! if (item == NULL) ! clear_tv(&listtv); ! else { ! item->value = alloc_tv(); ! if (item->value == NULL) { ! vim_free(item); ! clear_tv(&listtv); ! } ! else ! { ! *item->value = listtv; ! item->prev = head->prev; ! head->prev = item; ! item->next = head; ! item->prev->next = item; } } } } /* ! * Remove "node" from the queue that it is in and free it. ! * Caller should have freed or used node->value. */ static void ! remove_json_node(jsonq_T *node) { ! node->prev->next = node->next; ! node->next->prev = node->prev; ! vim_free(node); ! } ! /* ! * Get a message from the JSON queue for channel "ch_idx". ! * When "id" is positive it must match the first number in the list. ! * When "id" is zero or negative jut get the first message. ! * Return OK when found and return the value in "rettv". ! * Return FAIL otherwise. ! */ ! static int ! channel_get_json(int ch_idx, int id, typval_T **rettv) ! { ! jsonq_T *head = &channels[ch_idx].ch_json_head; ! jsonq_T *item = head->next; ! while (item != head) ! { ! list_T *l = item->value->vval.v_list; ! typval_T *tv = &l->lv_first->li_tv; ! ! if ((id > 0 && tv->v_type == VAR_NUMBER && tv->vval.v_number == id) ! || id <= 0) ! { ! *rettv = item->value; ! remove_json_node(item); ! return OK; ! } ! item = item->next; ! } ! return FAIL; } /* *************** *** 524,530 **** { exarg_T ea; ! ea.forceit = *arg != NUL; ex_redraw(&ea); showruler(FALSE); setcursor(); --- 647,653 ---- { exarg_T ea; ! ea.forceit = arg != NULL && *arg != NUL; ex_redraw(&ea); showruler(FALSE); setcursor(); *************** *** 577,646 **** static void may_invoke_callback(int idx) { ! char_u *msg; ! typval_T typetv; typval_T argv[3]; - typval_T arg3; - char_u *cmd = NULL; int seq_nr = -1; ! int ret = OK; if (channel_peek(idx) == NULL) return; ! /* Concatenate everything into one buffer. ! * TODO: only read what the callback will use. ! * TODO: avoid multiple allocations. */ ! while (channel_collapse(idx) == OK) ! ; ! msg = channel_get(idx); ! ! if (channels[idx].ch_json_mode) { ! ret = channel_decode_json(msg, &typetv, &argv[1], &arg3); ! if (ret == OK) { ! /* TODO: error if arg3 is set when it shouldn't? */ ! if (typetv.v_type == VAR_STRING) ! cmd = typetv.vval.v_string; ! else if (typetv.v_type == VAR_NUMBER) ! seq_nr = typetv.vval.v_number; } - } - else - { - argv[1].v_type = VAR_STRING; - argv[1].vval.v_string = msg; - } ! if (ret == OK) ! { ! if (cmd != NULL) { ! channel_exe_cmd(idx, cmd, &argv[1], &arg3); } ! else if (channels[idx].ch_req_callback != NULL && seq_nr != 0) ! { ! /* TODO: check the sequence number */ ! /* invoke the one-time callback */ ! invoke_callback(idx, channels[idx].ch_req_callback, argv); ! channels[idx].ch_req_callback = NULL; ! } ! else if (channels[idx].ch_callback != NULL) { ! /* invoke the channel callback */ ! invoke_callback(idx, channels[idx].ch_callback, argv); } - /* else: drop the message */ ! if (channels[idx].ch_json_mode) { ! clear_tv(&typetv); ! clear_tv(&argv[1]); ! clear_tv(&arg3); } } vim_free(msg); } --- 700,788 ---- static void may_invoke_callback(int idx) { ! char_u *msg = NULL; ! typval_T *listtv = NULL; ! list_T *list; ! typval_T *typetv; typval_T argv[3]; int seq_nr = -1; ! int json_mode = channels[idx].ch_json_mode; if (channel_peek(idx) == NULL) return; + if (channels[idx].ch_close_cb != NULL) + /* this channel is handled elsewhere (netbeans) */ + return; ! if (json_mode) { ! /* Get any json message. Return if there isn't one. */ ! channel_read_json(idx); ! if (channel_get_json(idx, -1, &listtv) == FAIL) ! return; ! if (listtv->v_type != VAR_LIST) { ! /* TODO: give error */ ! clear_tv(listtv); ! return; } ! list = listtv->vval.v_list; ! if (list->lv_len < 2) { ! /* TODO: give error */ ! clear_tv(listtv); ! return; } ! ! argv[1] = list->lv_first->li_next->li_tv; ! typetv = &list->lv_first->li_tv; ! if (typetv->v_type == VAR_STRING) { ! typval_T *arg3 = NULL; ! char_u *cmd = typetv->vval.v_string; ! ! /* ["cmd", arg] */ ! if (list->lv_len == 3) ! arg3 = &list->lv_last->li_tv; ! channel_exe_cmd(idx, cmd, &argv[1], arg3); ! clear_tv(listtv); ! return; } ! if (typetv->v_type != VAR_NUMBER) { ! /* TODO: give error */ ! clear_tv(listtv); ! return; } + seq_nr = typetv->vval.v_number; + } + else + { + /* For a raw channel we don't know where the message ends, just get + * everything. */ + msg = channel_get_all(idx); + argv[1].v_type = VAR_STRING; + argv[1].vval.v_string = msg; + } + + if (channels[idx].ch_req_callback != NULL && seq_nr != 0) + { + /* TODO: check the sequence number */ + /* invoke the one-time callback */ + invoke_callback(idx, channels[idx].ch_req_callback, argv); + channels[idx].ch_req_callback = NULL; + } + else if (channels[idx].ch_callback != NULL) + { + /* invoke the channel callback */ + invoke_callback(idx, channels[idx].ch_callback, argv); } + /* else: drop the message TODO: give error */ + if (listtv != NULL) + clear_tv(listtv); vim_free(msg); } *************** *** 661,677 **** void channel_close(int idx) { ! channel_T *channel = &channels[idx]; if (channel->ch_fd >= 0) { sock_close(channel->ch_fd); channel->ch_fd = -1; #ifdef FEAT_GUI channel_gui_unregister(idx); #endif vim_free(channel->ch_callback); channel->ch_callback = NULL; } } --- 803,831 ---- void channel_close(int idx) { ! channel_T *channel = &channels[idx]; ! jsonq_T *jhead; if (channel->ch_fd >= 0) { sock_close(channel->ch_fd); channel->ch_fd = -1; + channel->ch_close_cb = NULL; #ifdef FEAT_GUI channel_gui_unregister(idx); #endif vim_free(channel->ch_callback); channel->ch_callback = NULL; + + while (channel_peek(idx) != NULL) + vim_free(channel_get(idx)); + + jhead = &channel->ch_json_head; + while (jhead->next != jhead) + { + clear_tv(jhead->next->value); + remove_json_node(jhead->next); + } } } *************** *** 682,691 **** int channel_save(int idx, char_u *buf, int len) { ! queue_T *node; ! queue_T *head = &channels[idx].ch_head; ! node = (queue_T *)alloc(sizeof(queue_T)); if (node == NULL) return FAIL; /* out of memory */ node->buffer = alloc(len + 1); --- 836,845 ---- int channel_save(int idx, char_u *buf, int len) { ! readq_T *node; ! readq_T *head = &channels[idx].ch_head; ! node = (readq_T *)alloc(sizeof(readq_T)); if (node == NULL) return FAIL; /* out of memory */ node->buffer = alloc(len + 1); *************** *** 697,708 **** mch_memmove(node->buffer, buf, (size_t)len); node->buffer[len] = NUL; - if (head->next == NULL) /* initialize circular queue */ - { - head->next = head; - head->prev = head; - } - /* insert node at tail of queue */ node->next = head; node->prev = head->prev; --- 851,856 ---- *************** *** 726,732 **** char_u * channel_peek(int idx) { ! queue_T *head = &channels[idx].ch_head; if (head->next == head || head->next == NULL) return NULL; --- 874,880 ---- char_u * channel_peek(int idx) { ! readq_T *head = &channels[idx].ch_head; if (head->next == head || head->next == NULL) return NULL; *************** *** 734,801 **** } /* - * Return the first buffer from the channel and remove it. - * The caller must free it. - * Returns NULL if there is nothing. - */ - char_u * - channel_get(int idx) - { - queue_T *head = &channels[idx].ch_head; - queue_T *node; - char_u *p; - - if (head->next == head || head->next == NULL) - return NULL; - node = head->next; - /* dispose of the node but keep the buffer */ - p = node->buffer; - head->next = node->next; - node->next->prev = node->prev; - vim_free(node); - return p; - } - - /* - * Collapses the first and second buffer in the channel "idx". - * Returns FAIL if that is not possible. - */ - int - channel_collapse(int idx) - { - queue_T *head = &channels[idx].ch_head; - queue_T *node = head->next; - char_u *p; - - if (node == head || node == NULL || node->next == head) - return FAIL; - - p = alloc((unsigned)(STRLEN(node->buffer) - + STRLEN(node->next->buffer) + 1)); - if (p == NULL) - return FAIL; /* out of memory */ - STRCPY(p, node->buffer); - STRCAT(p, node->next->buffer); - vim_free(node->next->buffer); - node->next->buffer = p; - - /* dispose of the node and buffer */ - head->next = node->next; - node->next->prev = node->prev; - vim_free(node->buffer); - vim_free(node); - return OK; - } - - /* * Clear the read buffer on channel "idx". */ void channel_clear(int idx) { ! queue_T *head = &channels[idx].ch_head; ! queue_T *node = head->next; ! queue_T *next; while (node != NULL && node != head) { --- 882,895 ---- } /* * Clear the read buffer on channel "idx". */ void channel_clear(int idx) { ! readq_T *head = &channels[idx].ch_head; ! readq_T *node = head->next; ! readq_T *next; while (node != NULL && node != head) { *************** *** 947,954 **** } /* ! * Read from channel "idx". Blocks until there is something to read or the ! * timeout expires. * Returns what was read in allocated memory. * Returns NULL in case of error or timeout. */ --- 1041,1048 ---- } /* ! * Read from raw channel "idx". Blocks until there is something to read or ! * the timeout expires. * Returns what was read in allocated memory. * Returns NULL in case of error or timeout. */ *************** *** 964,975 **** channel_read(idx); } ! /* Concatenate everything into one buffer. ! * TODO: avoid multiple allocations. */ ! while (channel_collapse(idx) == OK) ! ; ! return channel_get(idx); } # if defined(WIN32) || defined(PROTO) --- 1058,1089 ---- channel_read(idx); } ! return channel_get_all(idx); ! } ! /* ! * Read one JSON message from channel "ch_idx" with ID "id" and store the ! * result in "rettv". ! * Blocks until the message is received. ! */ ! int ! channel_read_json_block(int ch_idx, int id, typval_T **rettv) ! { ! for (;;) ! { ! channel_read_json(ch_idx); ! ! /* search for messsage "id" */ ! if (channel_get_json(ch_idx, id, rettv) == OK) ! return OK; ! ! /* Wait for up to 2 seconds. ! * TODO: use timeout set on the channel. */ ! if (channel_wait(channels[ch_idx].ch_fd, 2000) == FAIL) ! break; ! channel_read(ch_idx); ! } ! return FAIL; } # if defined(WIN32) || defined(PROTO) *** ../vim-7.4.1230/src/json.c 2016-01-31 20:24:09.966066885 +0100 --- src/json.c 2016-01-31 22:49:58.975253082 +0100 *************** *** 549,562 **** /* * Decode the JSON from "reader" and store the result in "res". */ ! void json_decode(js_read_T *reader, typval_T *res) { json_skip_white(reader); json_decode_item(reader, res); json_skip_white(reader); if (reader->js_buf[reader->js_used] != NUL) ! EMSG(_(e_invarg)); } #endif --- 549,564 ---- /* * Decode the JSON from "reader" and store the result in "res". + * Return OK or FAIL; */ ! int json_decode(js_read_T *reader, typval_T *res) { json_skip_white(reader); json_decode_item(reader, res); json_skip_white(reader); if (reader->js_buf[reader->js_used] != NUL) ! return FAIL; ! return OK; } #endif *** ../vim-7.4.1230/src/proto/eval.pro 2016-01-28 22:36:15.052065044 +0100 --- src/proto/eval.pro 2016-01-31 23:02:33.143409845 +0100 *************** *** 101,106 **** --- 101,107 ---- char_u *v_exception(char_u *oldval); char_u *v_throwpoint(char_u *oldval); char_u *set_cmdarg(exarg_T *eap, char_u *oldarg); + typval_T *alloc_tv(void); void free_tv(typval_T *varp); void clear_tv(typval_T *varp); long get_tv_number_chk(typval_T *varp, int *denote); *** ../vim-7.4.1230/src/proto/channel.pro 2016-01-31 20:24:09.970066843 +0100 --- src/proto/channel.pro 2016-01-31 22:55:53.875563126 +0100 *************** *** 4,10 **** void channel_set_json_mode(int idx, int json_mode); void channel_set_callback(int idx, char_u *callback); void channel_set_req_callback(int idx, char_u *callback); - int channel_decode_json(char_u *msg, typval_T *tv1, typval_T *tv2, typval_T *tv3); int channel_is_open(int idx); void channel_close(int idx); int channel_save(int idx, char_u *buf, int len); --- 4,9 ---- *************** *** 15,20 **** --- 14,21 ---- int channel_get_id(void); void channel_read(int idx); char_u *channel_read_block(int idx); + int channel_read_json_block(int ch_idx, int id, typval_T **rettv); + void channel_read_json(int ch_idx); int channel_socket2idx(sock_T fd); int channel_send(int idx, char_u *buf, char *fun); int channel_poll_setup(int nfd_in, void *fds_in); *** ../vim-7.4.1230/src/proto/json.pro 2016-01-31 20:24:09.970066843 +0100 --- src/proto/json.pro 2016-01-31 22:56:25.115238152 +0100 *************** *** 1,5 **** /* json.c */ char_u *json_encode(typval_T *val); char_u *json_encode_nr_expr(int nr, typval_T *val); ! void json_decode(js_read_T *reader, typval_T *res); /* vim: set ft=c : */ --- 1,5 ---- /* json.c */ char_u *json_encode(typval_T *val); char_u *json_encode_nr_expr(int nr, typval_T *val); ! int json_decode(js_read_T *reader, typval_T *res); /* vim: set ft=c : */ *** ../vim-7.4.1230/src/structs.h 2016-01-29 23:20:35.313308119 +0100 --- src/structs.h 2016-01-31 21:19:43.711391593 +0100 *************** *** 2693,2697 **** char_u *js_end; /* NUL in js_buf when js_eof is FALSE */ int js_used; /* bytes used from js_buf */ int js_eof; /* when TRUE js_buf is all there is */ ! FILE *js_fd; /* file descriptor to read more from */ } js_read_T; --- 2693,2698 ---- char_u *js_end; /* NUL in js_buf when js_eof is FALSE */ int js_used; /* bytes used from js_buf */ int js_eof; /* when TRUE js_buf is all there is */ ! int (*js_fill)(void *); /* function to fill the buffer */ ! void *js_cookie; /* passed to js_fill */ } js_read_T; *** ../vim-7.4.1230/src/version.c 2016-02-01 21:32:51.622375175 +0100 --- src/version.c 2016-02-01 21:35:02.833003291 +0100 *************** *** 744,745 **** --- 744,747 ---- { /* Add new patch number below this line */ + /**/ + 1231, /**/ -- From "know your smileys": |-( Contact lenses, but has lost them /// Bram Moolenaar -- Bram@Moolenaar.net -- http://www.Moolenaar.net \\\ /// sponsor Vim, vote for features -- http://www.Vim.org/sponsor/ \\\ \\\ an exciting new programming language -- http://www.Zimbu.org /// \\\ help me help AIDS victims -- http://ICCF-Holland.org ///