patch 9.1.1485: missing Wayland clipboard support

Problem:  missing Wayland clipboard support
Solution: make it work (Foxe Chen)

fixes: #5157
closes: #17097

Signed-off-by: Foxe Chen <chen.foxe@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
diff --git a/src/clipboard.c b/src/clipboard.c
index fb967dc..4d9455a 100644
--- a/src/clipboard.c
+++ b/src/clipboard.c
@@ -31,6 +31,32 @@
 
 #if defined(FEAT_CLIPBOARD) || defined(PROTO)
 
+#if defined(FEAT_WAYLAND_CLIPBOARD)
+// Mime types we support sending and receiving
+// Mimes with a lower index in the array are prioritized first when we are
+// receiving data.
+static const char *supported_mimes[] = {
+    VIMENC_ATOM_NAME,
+    VIM_ATOM_NAME,
+    "text/plain;charset=utf-8",
+    "text/plain",
+    "UTF8_STRING",
+    "STRING",
+    "TEXT"
+};
+
+static void clip_wl_receive_data(Clipboard_T *cbd,
+	const char *mime_type, int fd);
+static void clip_wl_send_data(const char *mime_type, int fd,
+	wayland_selection_T);
+static void clip_wl_selection_cancelled(wayland_selection_T selection);
+
+#if defined(USE_SYSTEM) && defined(PROTO)
+static int clip_wl_owner_exists(Clipboard_T *cbd);
+#endif
+
+#endif
+
 /*
  * Selection stuff using Visual mode, for cutting and pasting text to other
  * windows.
@@ -50,6 +76,10 @@
     cb = &clip_star;
     for (;;)
     {
+	// No need to init again if cbd is already available
+	if (can_use && cb->available)
+	    goto skip;
+
 	cb->available  = can_use;
 	cb->owned      = FALSE;
 	cb->start.lnum = 0;
@@ -58,6 +88,7 @@
 	cb->end.col    = 0;
 	cb->state      = SELECT_CLEARED;
 
+skip:
 	if (cb == &clip_plus)
 	    break;
 	cb = &clip_plus;
@@ -109,13 +140,27 @@
     static int
 clip_gen_own_selection(Clipboard_T *cbd)
 {
-#ifdef FEAT_XCLIPBOARD
+#if defined(FEAT_XCLIPBOARD) || defined(FEAT_WAYLAND_CLIPBOARD)
 # ifdef FEAT_GUI
     if (gui.in_use)
 	return clip_mch_own_selection(cbd);
     else
 # endif
-	return clip_xterm_own_selection(cbd);
+    {
+	if (clipmethod == CLIPMETHOD_WAYLAND)
+	{
+#ifdef FEAT_WAYLAND_CLIPBOARD
+	    return clip_wl_own_selection(cbd);
+#endif
+	}
+	else if (clipmethod == CLIPMETHOD_X11)
+	{
+#ifdef FEAT_XCLIPBOARD
+	    return clip_xterm_own_selection(cbd);
+#endif
+	}
+    }
+    return FAIL;
 #else
     return clip_mch_own_selection(cbd);
 #endif
@@ -128,7 +173,7 @@
      * Also want to check somehow that we are reading from the keyboard rather
      * than a mapping etc.
      */
-#ifdef FEAT_X11
+#if defined(FEAT_X11) || defined(FEAT_WAYLAND_CLIPBOARD)
     // Always own the selection, we might have lost it without being
     // notified, e.g. during a ":sh" command.
     if (cbd->available)
@@ -160,13 +205,26 @@
     static void
 clip_gen_lose_selection(Clipboard_T *cbd)
 {
-#ifdef FEAT_XCLIPBOARD
+#if defined(FEAT_XCLIPBOARD) || defined(FEAT_WAYLAND_CLIPBOARD)
 # ifdef FEAT_GUI
     if (gui.in_use)
 	clip_mch_lose_selection(cbd);
     else
 # endif
-	clip_xterm_lose_selection(cbd);
+    {
+	if (clipmethod == CLIPMETHOD_WAYLAND)
+	{
+#ifdef FEAT_WAYLAND_CLIPBOARD
+	    clip_wl_lose_selection(cbd);
+#endif
+	}
+	else if (clipmethod == CLIPMETHOD_X11)
+	{
+#ifdef FEAT_XCLIPBOARD
+	    clip_xterm_lose_selection(cbd);
+#endif
+	}
+    }
 #else
     clip_mch_lose_selection(cbd);
 #endif
@@ -196,9 +254,9 @@
 	// windows on the current buffer.
 	if (was_owned
 		&& (get_real_state() == MODE_VISUAL
-					    || get_real_state() == MODE_SELECT)
+		    || get_real_state() == MODE_SELECT)
 		&& (cbd == &clip_star ?
-				clip_isautosel_star() : clip_isautosel_plus())
+		    clip_isautosel_star() : clip_isautosel_plus())
 		&& HL_ATTR(HLF_V) != HL_ATTR(HLF_VNC)
 		&& !exiting)
 	{
@@ -1195,13 +1253,26 @@
 	    return;
 	}
     }
-#ifdef FEAT_XCLIPBOARD
+#if defined(FEAT_XCLIPBOARD) || defined(FEAT_WAYLAND_CLIPBOARD)
 # ifdef FEAT_GUI
     if (gui.in_use)
 	clip_mch_set_selection(cbd);
     else
 # endif
-	clip_xterm_set_selection(cbd);
+    {
+	if (clipmethod == CLIPMETHOD_WAYLAND)
+	{
+#ifdef FEAT_WAYLAND_CLIPBOARD
+	    clip_wl_set_selection(cbd);
+#endif
+	}
+	else if (clipmethod == CLIPMETHOD_X11)
+	{
+#ifdef FEAT_XCLIPBOARD
+	    clip_xterm_set_selection(cbd);
+#endif
+	}
+    }
 #else
     clip_mch_set_selection(cbd);
 #endif
@@ -1210,13 +1281,26 @@
     static void
 clip_gen_request_selection(Clipboard_T *cbd)
 {
-#ifdef FEAT_XCLIPBOARD
+#if defined(FEAT_XCLIPBOARD) || defined(FEAT_WAYLAND_CLIPBOARD)
 # ifdef FEAT_GUI
     if (gui.in_use)
 	clip_mch_request_selection(cbd);
     else
 # endif
-	clip_xterm_request_selection(cbd);
+    {
+	if (clipmethod == CLIPMETHOD_WAYLAND)
+	{
+#ifdef FEAT_WAYLAND_CLIPBOARD
+	    clip_wl_request_selection(cbd);
+#endif
+	}
+	else if (clipmethod == CLIPMETHOD_X11)
+	{
+#ifdef FEAT_XCLIPBOARD
+	    clip_xterm_request_selection(cbd);
+#endif
+	}
+    }
 #else
     clip_mch_request_selection(cbd);
 #endif
@@ -1231,7 +1315,8 @@
 }
 #endif
 
-#if (defined(FEAT_X11) && defined(USE_SYSTEM)) || defined(PROTO)
+#if ((defined(FEAT_X11) || defined(FEAT_WAYLAND_CLIPBOARD)) \
+	&& defined(USE_SYSTEM)) || defined(PROTO)
     int
 clip_gen_owner_exists(Clipboard_T *cbd UNUSED)
 {
@@ -1241,7 +1326,22 @@
 	return clip_gtk_owner_exists(cbd);
     else
 # endif
-	return clip_x11_owner_exists(cbd);
+    {
+	if (clipmethod == CLIPMETHOD_WAYLAND)
+	{
+#ifdef FEAT_WAYLAND_CLIPBOARD
+	    return clip_wl_owner_exists(cbd);
+#endif
+	}
+	else if (clipmethod == CLIPMETHOD_X11)
+	{
+#ifdef FEAT_XCLIPBOARD
+	    return clip_x11_owner_exists(cbd);
+#endif
+	}
+	else
+	    return FALSE;
+    }
 #else
     return TRUE;
 #endif
@@ -2228,4 +2328,550 @@
     }
 }
 
+#if defined(FEAT_WAYLAND_CLIPBOARD) || defined(PROTO)
+
+/*
+ * Read data from a file descriptor and write it to the given clipboard.
+ */
+    static void
+clip_wl_receive_data(Clipboard_T *cbd, const char *mime_type, int fd)
+{
+    char_u	    *start, *buf, *tmp, *final, *enc;
+    int		    motion_type = MAUTO;
+    ssize_t	    r = 0;
+    size_t	    total = 0, max_total = 4096; // Initial buffer size, 4096
+						 // bytes seems reasonable.
+#ifndef HAVE_SELECT
+    struct pollfd   pfd
+
+    pfd.fd = fd,
+    pfd.events = POLLIN
+#else
+    fd_set rfds;
+    struct timeval  tv;
+
+    FD_ZERO(&rfds);
+    FD_SET(fd, &rfds);
+    tv.tv_sec = 0;
+    tv.tv_usec = p_wtm * 1000;
+#endif
+
+    // Make pipe (read end) non-blocking
+    if (fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK) == -1)
+	return;
+
+    if ((buf = alloc_clear(max_total)) == NULL)
+	return;
+    start = buf;
+
+    // Only poll before reading when we first start, then we do non-blocking
+    // reads and check for EAGAIN or EINTR to signal to poll again.
+    goto poll_data;
+
+    while (errno = 0, TRUE)
+    {
+	r = read(fd, start, max_total - 1 - total);
+
+	if (r == 0)
+	    break;
+	else if (r < 0)
+	{
+	    if (errno == EAGAIN || errno == EINTR)
+	    {
+poll_data:
+#ifndef HAVE_SELECT
+		if (poll(&pfd, 1, p_wtm) > 0)
+#else
+		if (select(fd + 1, &rfds, NULL, NULL, &tv) > 0)
+#endif
+		    continue;
+	    }
+	    break;
+	}
+
+	start += r;
+	total += (size_t)r;
+
+	// Realloc if we are at the end of the buffer
+	if (total >= max_total - 1)
+	{
+	    tmp = vim_realloc(buf, max_total * 2);
+	    if (tmp == NULL)
+		break;
+	    max_total *= 2; // Double buffer size each time
+	    buf = tmp;
+	    start = buf + total;
+	    // Zero out the newly allocated memory part
+	    vim_memset(buf + total, 0, max_total - total);
+	}
+    }
+
+    if (total == 0)
+    {
+	clip_free_selection(cbd); // Nothing received, clear register
+	vim_free(buf);
+	return;
+    }
+
+    final = buf;
+
+    if (STRCMP(mime_type, VIM_ATOM_NAME) == 0 && total >= 2)
+    {
+	motion_type = *final++;;
+	total--;
+    }
+    else if (STRCMP(mime_type, VIMENC_ATOM_NAME) == 0 && total >= 3)
+    {
+	vimconv_T conv;
+	int convlen;
+
+	// first byte is motion type
+	motion_type = *final++;
+	total--;
+
+	// Get encoding of selection
+	enc = final;
+
+	// Skip the encoding type including null terminator in final text
+	final += STRLEN(final) + 1;
+
+	// Subtract pointers to get length of encoding;
+	total -= final - enc;
+
+	conv.vc_type = CONV_NONE;
+	convert_setup(&conv, enc, p_enc);
+	if (conv.vc_type != CONV_NONE)
+	{
+	   convlen = total;
+	   tmp = string_convert(&conv, final, &convlen);
+	   total = convlen;
+	   if (tmp != NULL)
+		final = tmp;
+	   convert_setup(&conv, NULL, NULL);
+	}
+    }
+
+    clip_yank_selection(motion_type, final, (long)total, cbd);
+    vim_free(buf);
+}
+
+/*
+ * Get the current selection and fill the respective register for cbd with the
+ * data.
+ */
+    void
+clip_wl_request_selection(Clipboard_T *cbd)
+{
+    wayland_selection_T	    selection;
+    garray_T		    *mime_types;
+    int			    len;
+    int			    fd;
+    const char		    *chosen_mime = NULL;
+
+    if (cbd == &clip_star)
+	selection = WAYLAND_SELECTION_PRIMARY;
+    else if (cbd == &clip_plus)
+	selection = WAYLAND_SELECTION_REGULAR;
+    else
+	return;
+
+    // Get mime types that the source client offers
+    mime_types = wayland_cb_get_mime_types(selection);
+
+    if (mime_types == NULL || mime_types->ga_len == 0)
+    {
+	// Selection is empty/cleared
+	clip_free_selection(cbd);
+	return;
+    }
+
+    len = ARRAY_LENGTH(supported_mimes);
+
+    // Loop through and pick the one we want to receive from
+    for (int i = 0; i < len && chosen_mime == NULL; i++)
+    {
+	for (int k = 0; k < mime_types->ga_len && chosen_mime == NULL; k++)
+	{
+	    char *mime_type = ((char**)mime_types->ga_data)[k];
+
+	    if (STRCMP(mime_type, supported_mimes[i]) == 0)
+		chosen_mime = supported_mimes[i];
+	}
+    }
+    if (chosen_mime == NULL)
+	return;
+
+    fd = wayland_cb_receive_data(chosen_mime, selection);
+
+    if (fd == -1)
+	return;
+
+    // Start reading the file descriptor returned
+    clip_wl_receive_data(cbd, chosen_mime, fd);
+
+    close(fd);
+}
+
+/*
+ * Write data from either the clip or plus register, depending on the given
+ * selection, to the file descriptor that the receiving client will read from.
+ */
+    static void
+clip_wl_send_data(
+	const char	    *mime_type,
+	int		    fd,
+	wayland_selection_T selection)
+{
+    Clipboard_T	    *cbd;
+    long_u	    length;
+    char_u	    *string;
+    ssize_t	    written = 0;
+    size_t	    total = 0;
+    int		    did_vimenc = TRUE;
+    int		    did_motion_type = TRUE;
+    int		    motion_type;
+    int		    skip_len_check = FALSE;
+#ifndef HAVE_SELECT
+    struct pollfd   pfd
+
+    pfd.fd = fd,
+    pfd.events = POLLOUT
+#else
+    fd_set	    wfds;
+    struct timeval  tv;
+
+    FD_ZERO(&wfds);
+    FD_SET(fd, &wfds);
+    tv.tv_sec = 0;
+    tv.tv_usec = p_wtm * 1000;
+#endif
+    if (selection == WAYLAND_SELECTION_REGULAR)
+	cbd = &clip_plus;
+    else if (selection == WAYLAND_SELECTION_PRIMARY)
+	cbd = &clip_star;
+    else
+	return;
+
+    // Shouldn't happen unless there is a bug.
+    if (!cbd->owned)
+	return;
+
+    // Get the current selection
+    clip_get_selection(cbd);
+    motion_type = clip_convert_selection(&string, &length, cbd);
+
+    if (motion_type < 0)
+	goto exit;
+
+    if (STRCMP(mime_type, VIMENC_ATOM_NAME) == 0)
+    {
+	did_vimenc = FALSE;
+	did_motion_type = FALSE;
+    }
+    else if (STRCMP(mime_type, VIM_ATOM_NAME) == 0)
+	did_motion_type = FALSE;
+
+    while ((total < (size_t)length || skip_len_check) &&
+#ifndef HAVE_SELECT
+	   poll(&pfd, 1, p_wtm) > 0)
+#else
+	   select(fd + 1, NULL, &wfds, NULL, &tv) > 0)
+#endif
+    {
+	// First byte sent is motion type for vim specific formats
+	if (!did_motion_type)
+	{
+	    if (total == 1)
+	    {
+		total = 0;
+		did_motion_type = TRUE;
+		continue;
+	    }
+	    // We cast to char so that we only send one byte
+	    written = write( fd, (char_u*)&motion_type, 1);
+	   skip_len_check = TRUE;
+	}
+	else if (!did_vimenc)
+	{
+	    // For the vimenc format, after the first byte is the encoding type,
+	    // which is null terminated. Make sure we write that before writing
+	    // the actual selection.
+	   if (total == STRLEN(p_enc) + 1)
+	   {
+		total = 0;
+		did_vimenc = TRUE;
+		continue;
+	   }
+	   // Include null terminator
+	   written = write(fd, p_enc + total, STRLEN(p_enc) + 1 - total);
+	   skip_len_check = TRUE;
+	}
+	else
+	{
+	   // write the actual selection to the fd
+	   written = write(fd, string + total, length - total);
+	   if (skip_len_check)
+	       skip_len_check = FALSE;
+	}
+
+	if (written == -1)
+	   break;
+	total += written;
+    }
+exit:
+    vim_free(string);
+}
+
+/*
+ * Called if another client gains ownership of the given selection. If so then
+ * lose the selection internally.
+ */
+    static void
+clip_wl_selection_cancelled(wayland_selection_T selection)
+{
+    if (selection == WAYLAND_SELECTION_REGULAR)
+	clip_lose_selection(&clip_plus);
+    else if (selection == WAYLAND_SELECTION_PRIMARY)
+	clip_lose_selection(&clip_star);
+}
+
+/*
+ * Own the selection that cbd corresponds to. Start listening for requests from
+ * other Wayland clients so they can receive data from us. Returns OK on success
+ * and FAIL on failure.
+ */
+    int
+clip_wl_own_selection(Clipboard_T *cbd)
+{
+    wayland_selection_T selection;
+
+    if (cbd == &clip_star)
+	selection = WAYLAND_SELECTION_PRIMARY;
+    else if (cbd == &clip_plus)
+	selection = WAYLAND_SELECTION_REGULAR;
+    else
+	return FAIL;
+
+    return wayland_cb_own_selection(
+	clip_wl_send_data,
+	clip_wl_selection_cancelled,
+	supported_mimes,
+	sizeof(supported_mimes)/sizeof(*supported_mimes),
+	selection);
+}
+
+/*
+ * Disown the selection that cbd corresponds to. Note that the the cancelled
+ * event is not sent when the data source is destroyed.
+ */
+    void
+clip_wl_lose_selection(Clipboard_T *cbd)
+{
+    if (cbd == &clip_plus)
+	wayland_cb_lose_selection(WAYLAND_SELECTION_REGULAR);
+    else if (cbd == &clip_star)
+	wayland_cb_lose_selection(WAYLAND_SELECTION_PRIMARY);
+
+    /* wayland_cb_lose_selection(selection); */
+}
+
+/*
+ * Send the current selection to the clipboard. Do nothing for wayland because
+ * we will fill in the selection only when requested by another client.
+ */
+    void
+clip_wl_set_selection(Clipboard_T *cbd UNUSED)
+{
+}
+
+#if defined(USE_SYSTEM) && defined(PROTO)
+/*
+ * Return TRUE if we own the selection corresponding to cbd
+ */
+    static int
+clip_wl_owner_exists(Clipboard_T *cbd)
+{
+    if (cbd == &clip_plus)
+	return wayland_cb_selection_is_owned(WAYLAND_SELECTION_REGULAR);
+    else if (cbd == &clip_star)
+	return wayland_cb_selection_is_owned(WAYLAND_SELECTION_PRIMARY);
+}
+#endif
+
+#endif // FEAT_WAYLAND_CLIPBOARD
+
+
+/*
+ * Returns the first method for accessing the clipboard that is available/works,
+ * depending on the order of values in str.
+ */
+    static clipmethod_T
+get_clipmethod(char_u *str)
+{
+    int		len	= (int)STRLEN(str) + 1;
+    char_u	*buf	= alloc(len);
+
+    if (buf == NULL)
+	return CLIPMETHOD_FAIL;
+
+    clipmethod_T ret = CLIPMETHOD_FAIL;
+    char_u	*p = str;
+
+    while (*p != NUL)
+    {
+	clipmethod_T method = CLIPMETHOD_NONE;
+
+	(void)copy_option_part(&p, buf, len, ",");
+
+	if (STRCMP(buf, "wayland") == 0)
+	{
+#ifdef FEAT_WAYLAND_CLIPBOARD
+	    if (wayland_cb_is_ready())
+		method = CLIPMETHOD_WAYLAND;
+#endif
+	}
+	else if (STRCMP(buf, "x11") == 0)
+	{
+#ifdef FEAT_XCLIPBOARD
+	    // x_IOerror_handler() in os_unix.c should set xterm_dpy to NULL if
+	    // we lost connection to the X server.
+	    if (xterm_dpy != NULL)
+	    {
+		// If the X connection is lost then that handler will longjmp
+		// somewhere else, in that case we will call choose_clipmethod()
+		// again from there, and this if block won't be executed since
+		// xterm_dpy will be set to NULL.
+		xterm_update();
+		method = CLIPMETHOD_X11;
+	    }
+#endif
+	}
+	else
+	{
+	    ret = CLIPMETHOD_FAIL;
+	    goto exit;
+	}
+
+	// Keep on going in order to catch errors
+	if (method != CLIPMETHOD_NONE && ret == CLIPMETHOD_FAIL)
+	    ret = method;
+    }
+
+    // No match found, use "none".
+    ret = (ret == CLIPMETHOD_FAIL) ? CLIPMETHOD_NONE : ret;
+
+exit:
+    vim_free(buf);
+    return ret;
+}
+
+
+/*
+ * Returns name of clipmethod in a statically allocated string.
+ */
+    static char *
+clipmethod_to_str(clipmethod_T method)
+{
+    switch(method)
+    {
+	case CLIPMETHOD_WAYLAND:
+	    return "wayland";
+	case CLIPMETHOD_X11:
+	    return "x11";
+	default:
+	    return "none";
+    }
+}
+
+/*
+ * Sets the current clipmethod to use given by `get_clipmethod()`. Returns an
+ * error message on failure else NULL.
+ */
+    char *
+choose_clipmethod(void)
+{
+    // We call get_clipmethod first so that we can catch any errors, even if
+    // clipmethod is useless
+    clipmethod_T method = get_clipmethod(p_cpm);
+
+    if (method == CLIPMETHOD_FAIL)
+	return e_invalid_argument;
+
+// If GUI is running or we are not on a system with wayland or x11, then always
+// return CLIPMETHOD_NONE. System or GUI clipboard handling always overrides.
+#if defined(FEAT_XCLIPBOARD) || defined(FEAT_WAYLAND_CLIPBOARD)
+#if defined(FEAT_GUI)
+    if (gui.in_use)
+    {
+#ifdef FEAT_WAYLAND
+	// We only interact with wayland for the clipboard, we can just deinit
+	// everything.
+	wayland_uninit_client();
+#endif
+
+	method = CLIPMETHOD_NONE;
+	goto lose_sel_exit;
+    }
+#endif
+#else
+    // If on a system like windows or macos, then clipmethod is irrelevant, we
+    // use their way of accessing the clipboard.
+    method = CLIPMETHOD_NONE;
+    goto exit;
+#endif
+
+    // Deinitialize clipboard if there is no way to access clipboard
+    if (method == CLIPMETHOD_NONE)
+	clip_init(FALSE);
+    // If we have a clipmethod that works now, then initialize clipboard
+    else if (clipmethod == CLIPMETHOD_NONE
+	    && method != CLIPMETHOD_NONE)
+    {
+	clip_init(TRUE);
+	did_warn_clipboard = FALSE;
+    }
+
+    // Disown clipboard if we are switching to a new method
+    if (clipmethod != CLIPMETHOD_NONE && method != clipmethod)
+    {
+#if (defined(FEAT_XCLIPBOARD) || defined(FEAT_WAYLAND_CLIPBOARD)) \
+	&& defined(FEAT_GUI)
+lose_sel_exit:
+#endif
+	if (clip_star.owned)
+	    clip_lose_selection(&clip_star);
+	if (clip_plus.owned)
+	    clip_lose_selection(&clip_plus);
+    }
+
+#if !defined(FEAT_XCLIPBOARD) && !defined(FEAT_WAYLAND_CLIPBOARD)
+exit:
+#endif
+
+    clipmethod = method;
+
+#ifdef FEAT_EVAL
+    set_vim_var_string(VV_CLIPMETHOD, (char_u*)clipmethod_to_str(method), -1);
+#endif
+
+    return NULL;
+}
+
+/*
+ * Call choose_clipmethod().
+ */
+    void
+ex_clipreset(exarg_T *eap UNUSED)
+{
+    clipmethod_T prev = clipmethod;
+
+    choose_clipmethod();
+
+    if (clipmethod == CLIPMETHOD_NONE)
+	smsg(_("Could not find a way to access the clipboard."));
+    else if (clipmethod != prev)
+	smsg(_("Switched to clipboard method '%s'."),
+		clipmethod_to_str(clipmethod));
+}
+
 #endif // FEAT_CLIPBOARD