Index: doc/entry.n
==================================================================
--- doc/entry.n
+++ doc/entry.n
@@ -90,13 +90,23 @@
 and relief.  The \fBentry\fR command returns its
 \fIpathName\fR argument.  At the time this command is invoked,
 there must not exist a window named \fIpathName\fR, but
 \fIpathName\fR's parent must exist.
 .PP
-An entry is a widget that displays a one-line text string and
-allows that string to be edited using widget commands described below, which
+An entry is a widget that displays one line of text and
+allows that text to be edited using widget commands described below, which
 are typically bound to keystrokes and mouse actions.
+.PP
+The text is represented internally as a sequence of unicode code points.  If
+Tk is compiled with the flag USE_GLYPH_INDEXES then the text is viewed as a
+sequence of glyphs, each represented by a unicode grapheme cluster which may
+contain several unicode code points. (For example, the flag of Wales requires
+seven code points while some emojis require four.)  In this case, the word
+"character" should be understood to mean a glyph in the discussion below.  If
+the USE_GLYPH_INDEXES flag is not defined then it refers to a single unicode
+code point.
+.PP
 When first created, an entry's string is empty.
 A portion of the entry may be selected as described below.
 If an entry is exporting its selection (see the \fB\-exportselection\fR
 option), then it will observe the standard X11 protocols for handling the
 selection;  entry selections are available as type \fBSTRING\fR.
@@ -294,10 +304,18 @@
 .TP
 \fIpathName \fBinsert \fIindex string\fR
 Insert the characters of \fIstring\fR just before the character
 indicated by \fIindex\fR.  Returns an empty string.
 .TP
+\fIpathName \fBrange \fIfirst last\fR
+Returns a string consisting of the characters in the entry beginning with the
+character represented by the index \fIfirst\fR and ending with the character
+represented by the index \fIlast\fR.  The numerical values of \fIfirst\fR and
+\fIlast\fR can be arbitrary, but negative values are replaced by 0 while
+values larger than the length of the entry string is the length. An empyt
+string is returned if \fIfirst\fR > \fIlast\fR.
+.TP
 \fIpathName \fBscan\fR \fIoption args\fR
 This command is used to implement scanning on entries.  It has
 two forms, depending on \fIoption\fR:
 .RS
 .TP

Index: generic/tkEntry.c
==================================================================
--- generic/tkEntry.c
+++ generic/tkEntry.c
@@ -329,16 +329,16 @@
  * dispatch the entry widget command.
  */
 
 static const char *const entryCmdNames[] = {
     "bbox", "cget", "configure", "delete", "get", "icursor", "index",
-    "insert", "scan", "selection", "validate", "xview", NULL
+    "insert", "range", "scan", "selection", "validate", "xview", NULL
 };
 
 enum entryCmd {
     COMMAND_BBOX, COMMAND_CGET, COMMAND_CONFIGURE, COMMAND_DELETE,
-    COMMAND_GET, COMMAND_ICURSOR, COMMAND_INDEX, COMMAND_INSERT,
+    COMMAND_GET, COMMAND_ICURSOR, COMMAND_INDEX, COMMAND_INSERT, COMMAND_RANGE,
     COMMAND_SCAN, COMMAND_SELECTION, COMMAND_VALIDATE, COMMAND_XVIEW
 };
 
 static const char *const selCmdNames[] = {
     "adjust", "clear", "from", "present", "range", "to", NULL
@@ -482,19 +482,18 @@
  *--------------------------------------------------------------
  */
 
 int
 Tk_EntryObjCmd(
-    ClientData dummy,	/* NULL. */
+    ClientData dummy,		/* NULL. */
     Tcl_Interp *interp,		/* Current interpreter. */
     int objc,			/* Number of arguments. */
     Tcl_Obj *const objv[])	/* Argument objects. */
 {
     Entry *entryPtr;
     Tk_OptionTable optionTable;
     Tk_Window tkwin;
-    char *tmp;
     (void)dummy;
 
     if (objc < 2) {
 	Tcl_WrongNumArgs(interp, 1, objv, "pathName ?-option value ...?");
 	return TCL_ERROR;
@@ -529,13 +528,19 @@
     entryPtr->widgetCmd		= Tcl_CreateObjCommand(interp,
 	    Tk_PathName(entryPtr->tkwin), EntryWidgetObjCmd, entryPtr,
 	    EntryCmdDeletedProc);
     entryPtr->optionTable	= optionTable;
     entryPtr->type		= TK_ENTRY;
-    tmp				= (char *)ckalloc(1);
-    tmp[0]			= '\0';
-    entryPtr->string		= tmp;
+#ifndef USE_GLYPH_INDEXES
+    {
+	char *tmp = (char *)ckalloc(1);
+	tmp[0] = '\0';
+	entryPtr->string = tmp;
+    }
+#else
+    entryPtr->manager		= TkpTextManagerCreate(&entryPtr->string);
+#endif
     entryPtr->selectFirst	= -1;
     entryPtr->selectLast	= -1;
 
     entryPtr->cursor		= NULL;
     entryPtr->exportSelection	= 1;
@@ -733,10 +738,15 @@
 	}
 	if (GetEntryIndex(interp, entryPtr, objv[2],
 		&index) != TCL_OK) {
 	    goto error;
 	}
+
+#ifdef USE_GLYPH_INDEXES
+	index = TkpTextManagerContainingCluster(entryPtr->manager, index, NULL);
+#endif
+
 	Tcl_SetObjResult(interp, Tcl_NewWideIntObj(index));
 	break;
     }
 
     case COMMAND_INSERT: {
@@ -756,10 +766,52 @@
                 goto error;
             }
 	}
 	break;
     }
+
+    case COMMAND_RANGE: {
+	const char *substring;
+	int first, last;
+	if (objc != 4) {
+	    Tcl_WrongNumArgs(interp, 2, objv, "first last");
+	    goto error;
+	}
+	if (Tcl_GetIntFromObj(interp, objv[2], &first) != TCL_OK) {
+	    goto error;
+	}
+	if (Tcl_GetIntFromObj(interp, objv[3], &last) != TCL_OK) {
+	    goto error;
+	}
+#ifndef USE_GLYPH_INDEXES
+	if (first < 0) {
+	    first = 0;
+	}
+	if (last < first) {
+	    Tcl_SetObjResult(interp, Tcl_NewObj());
+	} else {
+	    const char *end;
+	    int length;
+	    Tcl_UniChar ch = 0;
+	    substring = Tcl_UtfAtIndex(entryPtr->displayString, first);
+	    length = Tcl_NumUtfChars(entryPtr->displayString, TCL_INDEX_NONE);
+	    if (last >= length) {
+		Tcl_SetObjResult(interp, Tcl_NewStringObj(substring, TCL_INDEX_NONE));
+	    } else {
+		end = Tcl_UtfAtIndex(entryPtr->displayString, last);
+		end += Tcl_UtfToUniChar(end, &ch);
+		Tcl_SetObjResult(interp, Tcl_NewStringObj(substring,
+		    end - substring));
+	    }
+	}
+#else
+	substring = TkpTextManagerUTF8StringForClusterRange(entryPtr->manager,
+		        first, last);
+	Tcl_SetObjResult(interp, Tcl_NewStringObj(substring, TCL_INDEX_NONE));
+#endif
+	break;
+    }
 
     case COMMAND_SCAN: {
 	int x;
 	const char *minorCmd;
 
@@ -1039,11 +1091,16 @@
     /*
      * Free up all the stuff that requires special handling, then let
      * Tk_FreeOptions handle all the standard option-related stuff.
      */
 
+#ifndef USE_GLYPH_INDEXES
     ckfree((char *)entryPtr->string);
+#else
+    TkpTextManagerDestroy(entryPtr->manager);
+#endif
+
     if (entryPtr->textVarName != NULL) {
 	Tcl_UntraceVar2(entryPtr->interp, entryPtr->textVarName,
 		NULL, TCL_GLOBAL_ONLY|TCL_TRACE_WRITES|TCL_TRACE_UNSETS,
 		EntryTextVarProc, entryPtr);
 	entryPtr->flags &= ~ENTRY_VAR_TRACED;
@@ -1765,11 +1822,10 @@
 
     /*
      * Draw the text in two pieces: first the unselected portion, then the
      * selected portion on top of it.
      */
-
     if ((entryPtr->numChars != 0) || (entryPtr->placeholderChars == 0)) {
         Tk_DrawTextLayout(entryPtr->display, pixmap, entryPtr->textGC,
 	    entryPtr->textLayout, entryPtr->layoutX, entryPtr->layoutY,
 	    entryPtr->leftIndex, entryPtr->numChars);
     } else {
@@ -2113,11 +2169,11 @@
 /*
  *----------------------------------------------------------------------
  *
  * InsertChars --
  *
- *	Add new characters to an entry widget.
+ *	Add new grapheme clusters to an entry widget.
  *
  * Results:
  *	A standard Tcl result. If an error occurred then an error message is
  *	left in the interp's result.
  *
@@ -2132,39 +2188,41 @@
 InsertChars(
     Entry *entryPtr,		/* Entry that is to get the new elements. */
     int index,			/* Add the new elements before this character
 				 * index. */
     const char *value)		/* New characters to add (NULL-terminated
-				 * string). */
+				 * UTF-8 encoded string). */
 {
-    size_t byteIndex, byteCount, newByteCount, oldChars, charsAdded;
-    const char *string;
+    int byteCount, oldChars, charsAdded;
     char *newStr;
 
-    string = entryPtr->string;
-    byteIndex = Tcl_UtfAtIndex(string, index) - string;
     byteCount = strlen(value);
     if (byteCount == 0) {
 	return TCL_OK;
     }
 
+#ifndef USE_GLYPH_INDEXES
+    size_t  byteIndex, newByteCount;
+    const char *string;
+    string = entryPtr->string;
+    byteIndex = Tcl_UtfAtIndex(string, index) - string;
+
     newByteCount = entryPtr->numBytes + byteCount + 1;
     newStr = (char *)ckalloc(newByteCount);
     memcpy(newStr, string, byteIndex);
     strcpy(newStr + byteIndex, value);
     strcpy(newStr + byteIndex + byteCount, string + byteIndex);
 
     if ((entryPtr->validate == VALIDATE_KEY ||
-	    entryPtr->validate == VALIDATE_ALL) &&
-	    EntryValidateChange(entryPtr, value, newStr, index,
-		    VALIDATE_INSERT) != TCL_OK) {
+	entryPtr->validate == VALIDATE_ALL) &&
+	EntryValidateChange(entryPtr, value, newStr, index,
+			    VALIDATE_INSERT) != TCL_OK) {
 	ckfree(newStr);
 	return TCL_OK;
     }
 
     ckfree((char *)string);
-    entryPtr->string = newStr;
 
     /*
      * The following construction is used because inserting improperly formed
      * UTF-8 sequences between other improperly formed UTF-8 sequences could
      * result in actually forming valid UTF-8 sequences; the number of
@@ -2175,15 +2233,35 @@
 
     oldChars = entryPtr->numChars;
     entryPtr->numChars = Tcl_NumUtfChars(newStr, TCL_INDEX_NONE);
     charsAdded = entryPtr->numChars - oldChars;
     entryPtr->numBytes += byteCount;
+#else
+    oldChars = entryPtr->numChars;
+    if (entryPtr->validate == VALIDATE_KEY ||
+	entryPtr->validate == VALIDATE_ALL) {
+	newStr = (char *) TkpTextManagerInsert(entryPtr->manager, index, value,
+			      &entryPtr->numChars, &entryPtr->numBytes,
+			      &entryPtr->string);
+	if (EntryValidateChange(entryPtr, value, newStr, index,
+				VALIDATE_INSERT) != TCL_OK) {
+	    entryPtr->string = TkpTextManagerRevert(entryPtr->manager, &entryPtr->numChars,
+				 &entryPtr->numBytes);
+	    return TCL_OK;
+	}
+    } else {
+	newStr = (char *) TkpTextManagerInsert(entryPtr->manager, index, value,
+			      &entryPtr->numChars, &entryPtr->numBytes, NULL);
+    }
+    charsAdded = entryPtr->numChars - oldChars;
+#endif
 
-    if (entryPtr->displayString == string) {
+    if (entryPtr->displayString == entryPtr->string) {
 	entryPtr->displayString = newStr;
 	entryPtr->numDisplayBytes = entryPtr->numBytes;
     }
+    entryPtr->string = newStr;
 
     /*
      * Inserting characters invalidates all indexes into the string. Touch up
      * the indexes so that they still refer to the same characters (at new
      * positions). When updating the selection end-points, don't include the
@@ -2212,11 +2290,11 @@
 /*
  *----------------------------------------------------------------------
  *
  * DeleteChars --
  *
- *	Remove one or more characters from an entry widget.
+ *	Remove one or more grapheme clusters from an entry widget.
  *
  * Results:
  *	A standard Tcl result. If an error occurred then an error message is
  *	left in the interp's result.
  *
@@ -2228,16 +2306,19 @@
  */
 
 static int
 DeleteChars(
     Entry *entryPtr,		/* Entry widget to modify. */
-    int index,			/* Index of first character to delete. */
-    int count)			/* How many characters to delete. */
+    int index,			/* Index of first cluster to delete. */
+    int count)			/* How many clusters to delete. */
 {
+    char *newStr;
+    const char *string = entryPtr->string;
+    char *toDelete;
+
+#ifndef USE_GLYPH_INDEXES
     int byteIndex, byteCount, newByteCount;
-    const char *string;
-    char *newStr, *toDelete;
 
     if ((index + count) > entryPtr->numChars) {
 	count = entryPtr->numChars - index;
     }
     if (count <= 0) {
@@ -2263,16 +2344,33 @@
 		    VALIDATE_DELETE) != TCL_OK) {
 	ckfree(newStr);
 	ckfree(toDelete);
 	return TCL_OK;
     }
-
     ckfree(toDelete);
     ckfree((char *)entryPtr->string);
-    entryPtr->string = newStr;
     entryPtr->numChars -= count;
     entryPtr->numBytes -= byteCount;
+#else
+    if ((entryPtr->validate == VALIDATE_KEY ||
+	 entryPtr->validate == VALIDATE_ALL)) {
+	newStr = (char *) TkpTextManagerDelete(entryPtr->manager, index, count,
+			      &entryPtr->numChars, &entryPtr->numBytes, &count,
+			      &toDelete, &entryPtr->string);
+	if (EntryValidateChange(entryPtr, toDelete, newStr, index,
+		    VALIDATE_DELETE) != TCL_OK) {
+	    entryPtr->string = TkpTextManagerRevert(entryPtr->manager, &entryPtr->numChars,
+				   &entryPtr->numBytes);
+	    return TCL_OK;
+	}
+    } else {
+	newStr = (char *) TkpTextManagerDelete(entryPtr->manager, index, count,
+			      &entryPtr->numChars, &entryPtr->numBytes, &count,
+			      NULL, NULL);
+    }
+#endif
+    entryPtr->string = newStr;
 
     if (entryPtr->displayString == string) {
 	entryPtr->displayString = newStr;
 	entryPtr->numDisplayBytes = entryPtr->numBytes;
     }
@@ -2461,10 +2559,12 @@
 	    return;
 	}
     }
 
     oldSource = entryPtr->string;
+
+#ifndef USE_GLYPH_INDEXES
     ckfree((char *)entryPtr->string);
 
     if (malloced) {
 	entryPtr->string = value;
     } else {
@@ -2473,14 +2573,18 @@
 	strcpy(tmp, value);
 	entryPtr->string = tmp;
     }
     entryPtr->numBytes = valueLen;
     entryPtr->numChars = Tcl_NumUtfChars(value, valueLen);
+#else
+    entryPtr->string = TkpTextManagerSet(entryPtr->manager, value,
+			   &entryPtr->numChars, &entryPtr->numBytes);
+#endif
 
     if (entryPtr->displayString == oldSource) {
 	entryPtr->displayString = entryPtr->string;
-	entryPtr->numDisplayBytes = entryPtr->numBytes;
+	entryPtr->numDisplayBytes = entryPtr->numChars;
     }
 
     if (entryPtr->selectFirst >= 0) {
 	if (entryPtr->selectFirst >= entryPtr->numChars) {
 	    entryPtr->selectFirst = -1;
@@ -2645,26 +2749,46 @@
 
 static int
 GetEntryIndex(
     Tcl_Interp *interp,		/* For error messages. */
     Entry *entryPtr,		/* Entry for which the index is being
-				 * specified. */
-    Tcl_Obj *indexObj,	/* Specifies character in entryPtr. */
+				   specified. */
+    Tcl_Obj *indexObj,		/* Specifies character in entryPtr. */
     int *indexPtr)		/* Where to store converted character index */
 {
     TkSizeT length, idx;
     const char *string;
 
+#ifndef USE_GLYPH_INDEXES
     if (TCL_OK == TkGetIntForIndex(indexObj, entryPtr->numChars - 1, 1, &idx)) {
 	if (idx == TCL_INDEX_NONE) {
 	    idx = 0;
 	} else if (idx > (TkSizeT)entryPtr->numChars) {
 	    idx = (TkSizeT)entryPtr->numChars;
 	}
 	*indexPtr = (int)idx;
 	return TCL_OK;
     }
+#else
+
+    /*
+     * If we are doing glyph indexing, integer objects are given as glyph
+     * indexes so we need to convert them to character indexes.
+     */
+
+    TkSizeT clusterLength = (TkSizeT) TkpTextManagerNumClusters(
+	                                  entryPtr->manager);
+    if (TCL_OK == TkGetIntForIndex(indexObj, clusterLength, 1, &idx)) {
+	if (idx == TCL_INDEX_NONE) {
+	    idx = 0;
+	} else if (idx > clusterLength) {
+	    idx = (TkSizeT) clusterLength + 1;
+	}
+	*indexPtr = TkpTextManagerClusterPosition(entryPtr->manager, idx, NULL);
+	return TCL_OK;
+    }
+#endif
 
     string = TkGetStringFromObj(indexObj, &length);
 
     switch (string[0]) {
     case 'a':

Index: generic/tkEntry.h
==================================================================
--- generic/tkEntry.h
+++ generic/tkEntry.h
@@ -36,19 +36,18 @@
     Tcl_Interp *interp;		/* Interpreter associated with entry. */
     Tcl_Command widgetCmd;	/* Token for entry's widget command. */
     Tk_OptionTable optionTable;	/* Table that defines configuration options
 				 * available for this widget. */
     enum EntryType type;	/* Specialized type of Entry widget */
+    ClientData manager;		/* Platform-specific TextManager. */
 
     /*
      * Fields that are set by widget commands other than "configure".
      */
 
-    const char *string;		/* Pointer to storage for string;
-				 * NULL-terminated; malloc-ed. */
-    int insertPos;		/* Character index before which next typed
-				 * character will be inserted. */
+    const char *string;		/* Pointer to storage for string. */
+    int insertPos;		/* Index of the character after the cursor */
 
     /*
      * Information about what's selected, if any.
      */
 

Index: generic/tkInt.h
==================================================================
--- generic/tkInt.h
+++ generic/tkInt.h
@@ -1347,12 +1347,51 @@
 			    Tcl_Interp *interp, Tcl_Obj *listObj,
 			    int toplevel, Tcl_Obj *nameObj);
 MODULE_SCOPE void	TkRotatePoint(double originX, double originY,
 			    double sine, double cosine, double *xPtr,
 			    double *yPtr);
-MODULE_SCOPE int TkGetIntForIndex(Tcl_Obj *, TkSizeT, int lastOK, TkSizeT*);
+MODULE_SCOPE int	TkGetIntForIndex(Tcl_Obj *, TkSizeT, int lastOK, TkSizeT*);
+
+/*
+ * Unicode strings describing text are actually sequences of so-called grapheme
+ * clusters, each of which describes what the user perceives as a single glyph.
+ * When editing text, users expect to insert or delete entire glyphs, so the
+ * underlying string operations should insert or delete entire grapheme clusters.
+ * Also, indexes into the string, such as the insert cursor, should refer to
+ * a glyph, not a character in the string and underlying character indexes should
+ * always point to the base character of a grapheme cluster.
+ *
+ * The functions declared below provide an interface to an abstract TextManager
+ * object which can recognize boundaries of grapheme clusters and manage a
+ * glyph-based indexing system for Tk text-related widgets.  To enable the features
+ * described above, a platform port should define the conditional compilation
+ * variable USE_GLYPH_INDEXES and implement these functions.
+ */
 
+#if defined(MAC_OSX_TK)
+#define USE_GLYPH_INDEXES
+MODULE_SCOPE ClientData  TkpTextManagerCreate(const char **initialString);
+MODULE_SCOPE void	 TkpTextManagerDestroy(ClientData clientData);
+MODULE_SCOPE int	 TkpTextManagerNumClusters(ClientData clientData);
+MODULE_SCOPE int	 TkpTextManagerClusterPosition(ClientData clientData,
+			     int clusterIndex, int *clusterLength);
+MODULE_SCOPE int	 TkpTextManagerContainingCluster(ClientData clientData,
+			     int charIndex, int *clusterLength);
+MODULE_SCOPE const char* TkpTextManagerUTF8StringForClusterRange(ClientData clientData,
+			     int first, int last);
+MODULE_SCOPE const char* TkpTextManagerSet(ClientData clientData,
+			     const char *value, int *numChars, int *numBytes);
+MODULE_SCOPE const char* TkpTextManagerInsert(ClientData clientData,
+			     int charIndex, const char *value,
+			     int *numChars, int *numBytes, const char **oldString);
+MODULE_SCOPE const char* TkpTextManagerDelete(ClientData clientData, int charIndex,
+			     int count, int *numChars, int *numBytes,
+			     int *numDeleted, char** charsDeleted,
+			     const char **oldString);
+MODULE_SCOPE const char* TkpTextManagerRevert(ClientData clientData,
+			     int *numChars, int *numBytes);
+#endif
 
 #ifdef _WIN32
 #define TkParseColor XParseColor
 #else
 MODULE_SCOPE Status TkParseColor (Display * display,

Index: library/entry.tcl
==================================================================
--- library/entry.tcl
+++ library/entry.tcl
@@ -674,11 +674,11 @@
 #
 # Arguments:
 # w -         The entry window from which the text to get
 
 proc ::tk::EntryGetSelection {w} {
-    set entryString [string range [$w get] [$w index sel.first] \
+    set entryString [$w range [$w index sel.first] \
 	    [expr {[$w index sel.last] - 1}]]
     if {[$w cget -show] ne ""} {
 	return [string repeat [string index [$w cget -show] 0] \
 		[string length $entryString]]
     }

Index: macosx/tkMacOSXFont.c
==================================================================
--- macosx/tkMacOSXFont.c
+++ macosx/tkMacOSXFont.c
@@ -182,11 +182,591 @@
 #ifndef __clang__
 @synthesize UTF8String = _UTF8String;
 #endif
 @end
 
-#define GetNSFontTraitsFromTkFontAttributes(faPtr) \
+/*
+ * Implementation of the TextManager for macOS.
+ *
+ * The TextManager is really little more than an NSMutableString, except
+ * that Apple says this about the UTF8String property:
+ *
+ *    "This C string is a pointer to a structure inside the string object,
+ *     which may have a lifetime shorter than the string object and will
+ *     certainly not have a longer lifetime. Therefore, you should copy the C
+ *     string if it needs to be stored outside of the memory context in which
+ *     you use this property."
+ *
+ */
+
+typedef struct TextManager {
+    NSMutableString *string;
+    NSMutableString *backup;
+    char *utf8string;       /* We need to store a copy of the UTF8String */
+    int numClusters;
+} TextManager;
+
+/*
+ * Static functions used to access a TextManager.
+ */
+
+/*
+ * Called after the NSMutableString has been changed.  It resets the counts of
+ * bytes and chars and saves a copy of the UTF8String of the NSMutableString.
+ */
+
+static char *
+TextManagerUpdate(
+    TextManager *managerPtr,
+    int *numChars,
+    int *numBytes)
+{
+    *numChars = [managerPtr->string length];
+    *numBytes = [managerPtr->string lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
+    if (managerPtr->utf8string) {
+	ckfree(managerPtr->utf8string);
+    }
+    managerPtr->utf8string = ckalloc(*numBytes + 1);
+    strcpy(managerPtr->utf8string, [managerPtr->string UTF8String]);
+    managerPtr->numClusters = -1;  /* recomputed by TkpTextManagerNumClusters */
+    return managerPtr->utf8string;
+}
+
+/*
+ * Destroy the cached NSString, if there is one.
+ */
+
+static void
+TextManagerClearCache(
+    TextManager *managerPtr)
+{
+    if (managerPtr->backup) {
+	[managerPtr->backup release];
+	managerPtr->backup = nil;
+    }
+}
+
+/*
+ * Cache a copy of the current string, for use when reverting changes after
+ * validation fails.
+ */
+
+static void
+TextManagerCache(
+    TextManager *managerPtr)
+{
+    TextManagerClearCache(managerPtr);
+    managerPtr->backup = [[NSMutableString stringWithString:managerPtr->string]
+			     retain];
+}
+
+/*
+ * Return the character range of of the cluster with given index, or a range
+ * with length 0 and location equal to the string length.
+ *
+ * NOTE: A future optimization could cache the character index of the last base
+ * character which was looked up inside the TextManager, as a hint.  Then the
+ * next time a base character index were needed, the search could begin at the
+ * hint location instead of the beginning of the string.  Since changes to the
+ * insert cursor are usually small, this would be quite a bit faster.
+ */
+
+static NSRange
+ClusterRange(
+    NSString *string,
+    NSUInteger clusterIndex)
+{
+    NSRange clusterRange = NSMakeRange(0, 0);
+    NSUInteger i, charIndex = 0, end = [string length];
+
+    if (end == 0) {
+	return NSMakeRange(0,0);
+    }
+    for (i = 0; i < clusterIndex; i++) {
+	clusterRange = [string rangeOfComposedCharacterSequenceAtIndex:charIndex];
+	charIndex = clusterRange.location + clusterRange.length;
+	if (charIndex >= end) {
+	    return NSMakeRange(charIndex, 0);
+	    break;
+	}
+    }
+    return [string rangeOfComposedCharacterSequenceAtIndex:charIndex];
+}
+
+/*
+ * Returns the index of the cluster which constains the the character with
+ * given index, or the total number of clusters.
+ *
+ * NOTE: This could also benefit from a cached hint.
+ */
+
+static NSUInteger
+IndexOfContainingCluster(
+    NSString *string,
+    NSUInteger charIndex,
+    int *clusterLength)
+{
+    NSRange clusterRange;
+    NSUInteger idx, clusterIndex;
+
+    if (charIndex > string.length) {
+	charIndex = string.length;
+    }
+    for (idx = 0, clusterIndex = 0; idx < charIndex; clusterIndex++) {
+	clusterRange = [string rangeOfComposedCharacterSequenceAtIndex:idx];
+	idx += clusterRange.length;
+	if (idx > charIndex) {
+	    if (clusterLength) {
+		clusterLength = 0;
+	    }
+	    return clusterIndex;
+	}
+    }
+    if (clusterLength) {
+	*clusterLength = clusterRange.length;
+    }
+
+    return clusterIndex;
+}
+
+/*
+ * Computes the range of characters filled by a range of clusters of given
+ * length, such that a given character is contained in the first cluster.
+ * Used by TkpTextManagerDelete to determine which chars to delete from
+ * the NSMutableString.
+ */
+
+static NSRange
+CharRangeFromClusterRange(
+    NSString *string,
+    NSUInteger charIndex,
+    NSUInteger clusterCount)
+{
+    NSRange clusterRange;
+    NSUInteger max = string.length, charLocation, charLength = 0;
+
+    if (max == 0 || charIndex >= max) {
+	return NSMakeRange(max, 0);
+    }
+    clusterRange = [string rangeOfComposedCharacterSequenceAtIndex:charIndex];
+    charLocation = clusterRange.location;
+    charIndex = charLocation;
+
+    while (clusterCount--) {
+	clusterRange = [string rangeOfComposedCharacterSequenceAtIndex:charIndex];
+	charLength += clusterRange.length;
+	charIndex += clusterRange.length;
+	if (charIndex >= max) {
+	    return NSMakeRange(charLocation, max - charLocation);
+	}
+    }
+    return NSMakeRange(charLocation, charLength);
+}
+
+/*
+ *---------------------------------------------------------------------------
+ *
+ *  TkpTextManagerCreate --
+ *
+ *	Allocate and initialize a TextManager to handle glyph-based indexing.
+ *
+ * Results:
+ *	A pointer to a TextManager, cast as an opaque ClientData type.
+ *
+ * Side effects:
+ *	Allocates a TextManager, an NSString and a UTF-8 char buffer.
+ *      Also stores a pointer to the initial (empty) UTF-8 string in
+ *      the variable referenced by the initialString parameter.
+ *
+ *---------------------------------------------------------------------------
+ */
+
+ClientData
+TkpTextManagerCreate(
+    const char **initialString)
+{
+    TextManager *managerPtr = (TextManager *) ckalloc(sizeof(TextManager));
+    int dummy;
+
+    managerPtr->string = [[NSMutableString string] retain];
+    managerPtr->backup = nil;
+    managerPtr->utf8string = NULL;
+    *initialString = TextManagerUpdate(managerPtr, &dummy, &dummy);
+    return (ClientData) managerPtr;
+}
+
+/*
+ *---------------------------------------------------------------------------
+ *
+ *  TkpTextManagerDestroy --
+ *
+ *	Free the resources associated to a TextManager
+ *
+ * Results:
+ *	None
+ *
+ * Side effects:
+ *	Release the NSString, frees the UTF8 string and the TextManager.
+ *
+ *---------------------------------------------------------------------------
+ */
+
+void
+TkpTextManagerDestroy(
+ClientData clientData)
+{
+    TextManager *managerPtr = (TextManager *) clientData;
+
+    [managerPtr->string release];
+    if (managerPtr->backup) {
+	[managerPtr->backup release];
+    }
+    if (managerPtr->utf8string) {
+	ckfree(managerPtr->utf8string);
+    }
+    ckfree(managerPtr);
+}
+
+/*
+ *---------------------------------------------------------------------------
+ *
+ *  TkpTextManagerClusterPosition --
+ *
+ *	Computes the character index of the base character of the cluster
+ *      with the given cluster index.  If the clusterLength parameter is
+ *      not NULL, the integer variable that it references is set to the
+ *      length of the cluster.
+ *
+ * Results:
+ *	A cluster index.
+ *
+ * Side effects:
+ *	None.
+ *
+ *---------------------------------------------------------------------------
+ */
+
+int
+TkpTextManagerClusterPosition(
+    ClientData clientData,
+    int clusterIndex,
+    int *clusterLength)
+{
+    TextManager *managerPtr = (TextManager *) clientData;
+    NSRange range;
+    if (clusterIndex < 0) {
+	clusterIndex = 0;
+    }
+    range = ClusterRange(managerPtr->string, clusterIndex);
+    if (clusterLength) {
+	*clusterLength = range.length;
+    }
+    return range.location; 
+}
+
+/*
+ *---------------------------------------------------------------------------
+ *
+ *  TkpTextManagerContainingCluster --
+ *
+ *	Given a character index, find the index of the cluster which contains
+ *      the indexed character.  If the parameter clusterLength is not NULL the
+ *      integer variable that it references is set to the length of the cluster.
+ *
+ * Results:
+ *	A cluster index.
+ *
+ * Side effects:
+ *	None.
+ *
+ *---------------------------------------------------------------------------
+ */
+
+int
+TkpTextManagerContainingCluster(
+    ClientData clientData,
+    int charIndex,
+    int *clusterLength)
+{
+    TextManager *managerPtr = (TextManager *) clientData;
+    if (charIndex < 0) {
+	return 0;
+    }
+    return (int) IndexOfContainingCluster(managerPtr->string, charIndex,
+		     clusterLength);
+}
+
+/*
+ *---------------------------------------------------------------------------
+ *
+ *  TkpTextManagerNumClusters --
+ *
+ *	Return the (cached) number of clusters in the entire NSMutableString.
+ *
+ * Results:
+ *	The number of clusters.
+ *
+ * Side effects:
+ *	None.
+ *
+ *---------------------------------------------------------------------------
+ */
+
+int
+TkpTextManagerNumClusters(
+    ClientData clientData)
+{
+    TextManager *managerPtr = (TextManager *) clientData;
+    NSString *string = managerPtr->string;
+
+    if (managerPtr->numClusters < 0) {
+	managerPtr->numClusters = IndexOfContainingCluster(string,
+				      string.length, NULL);
+    }
+    return managerPtr->numClusters;
+}
+/*
+ *---------------------------------------------------------------------------
+ *
+ *  TkpTextManagerCharRangeOfClusterRange --
+ *
+ *	Return a pointer to a UTF-8 encoded C string which represents a
+ *      substring of the text manager's string.  The result includes all
+ *      clusters with index >= first and <= last.  It is allowed for first
+ *      to be negative and last to be greater than or equal to numClusters.
+ *      The resulting pointer has an indeterminate lifespan since it is
+ *      equal to the UTF8String property of an NSString.  So it should be
+ *      copied or used before the current iteration of the event loop
+ *      terminates.
+ *
+ * Results:
+ *	The number of clusters.
+ *
+ * Side effects:
+ *	None.
+ *
+ *---------------------------------------------------------------------------
+ */
+
+const char *
+TkpTextManagerUTF8StringForClusterRange(
+    ClientData clientData,
+    int firstCluster,
+    int lastCluster)
+{
+    TextManager *managerPtr = (TextManager *) clientData;
+    NSRange clusterRange, fullRange;
+    NSUInteger location, charIndex, clusterIndex;
+    int length;
+    NSString *str = managerPtr->string, *temp;
+    static char empty = '\0';
+    
+    if (firstCluster < 0) {
+	firstCluster = 0;
+    }
+    if (lastCluster < firstCluster) {
+	return &empty;
+    }
+    clusterIndex = firstCluster;
+    location = TkpTextManagerClusterPosition(managerPtr, firstCluster, &length);
+    charIndex = location;
+    while (clusterIndex <= (unsigned int) lastCluster &&
+	   charIndex < [managerPtr->string length]) {
+	clusterRange = [str rangeOfComposedCharacterSequenceAtIndex:charIndex];
+	charIndex += clusterRange.length;
+	clusterIndex++;
+    }
+    fullRange = NSMakeRange(location, charIndex - location);
+    temp = [str substringWithRange:fullRange];
+    return [temp UTF8String];
+}
+
+/*
+ *---------------------------------------------------------------------------
+ *
+ *  TkpTextManagerSet --
+ *
+ *	Set the contents of the NSMutableString from a UTF-8 encoded string.
+ *
+ * Results:
+ *	A pointer to the TextManager's cached UTF-8 string.
+ *
+ * Side effects:
+ *	The number of unichars and bytes are recorded in the integer variables
+ *      referenced by the numChars and numBytes parameters.
+ *
+ *---------------------------------------------------------------------------
+ */
+
+const char *
+TkpTextManagerSet(
+    ClientData clientData,
+    const char *value,
+    int *numChars,
+    int *numBytes)
+{
+    TextManager *managerPtr = (TextManager *) clientData;
+    NSString *valueString = [[NSString alloc] initWithUTF8String: value];
+
+    [managerPtr->string setString:valueString];
+    return TextManagerUpdate(managerPtr, numChars, numBytes);
+}
+
+/*
+ *---------------------------------------------------------------------------
+ *
+ *  TkpTextManagerRevert --
+ *
+ *	Restore the previous state of the text from the cache.  This assumes
+ *      that the cache was created before making the last change to the text.
+ *      Otherwise it has no effect.
+ *
+ * Results:
+ *	A pointer to the UTF-8 string for the restored text.
+ *
+ * Side effects:
+ *	The TextManager's string is set to the cached string and the cache is
+ *      cleared.
+ *
+ *
+ *---------------------------------------------------------------------------
+ */
+
+const char *
+TkpTextManagerRevert(
+   ClientData clientData,
+   int *numChars,
+   int *numBytes)
+{
+    TextManager *managerPtr = (TextManager *) clientData;
+    if (managerPtr->backup) {
+        [managerPtr->string release];
+	managerPtr->string = managerPtr->backup;
+	managerPtr->backup = nil;
+    }
+    return TextManagerUpdate(managerPtr, numChars, numBytes);
+}
+
+/*
+ *---------------------------------------------------------------------------
+ *
+ *  TkpTextManagerInsert --
+ *
+ *	Insert the string, as described by the UTF-8 encoded byte array
+ *      referenced by the value parameter, at the provided character index.
+ *      Inserting a cluster may be done with sequential calls that each provide
+ *      a single unicode code point.  The macOS port always provides both
+ *      surrogate pairs in a single XEvent, so there should not be misplaced
+ *      surrogates, but the modifiers following the base char may be incomplete
+ *      after calling this.  Also, the provided character index may not refer
+ *      to a base character in some calls.  If the oldString parameter is not
+ *      NULL the pointer which it references will be set to the address of a
+ *      UTF-8 encoding of the text prior to the insertion.  This is intended for
+ *      use in validating the change to the string, and may not persist after
+ *      the next iteration of the event loop.
+ *
+ * Results:
+ *	A pointer to the TextManager's UTF-8 string.
+ *
+ * Side effects:
+ *      The number of unichars and bytes are recorded in the integer variables
+ *	referenced by the numChars and numBytes parameters.  The pointer
+ *	referenced by the parameter oldString may be set to the address of an
+ *	encoded byte sequence representing the cached prior state of the
+ *	string.
+ *
+ *---------------------------------------------------------------------------
+ */
+
+const char*
+TkpTextManagerInsert(
+    ClientData clientData,
+    int charIndex,
+    const char *value,
+    int *numChars,
+    int *numBytes,
+    const char **oldString)
+{
+    TextManager *managerPtr = (TextManager *) clientData;
+    NSString *valueString = [[NSString alloc] initWithUTF8String: value];
+    if (oldString) {
+	TextManagerCache(managerPtr);
+	[managerPtr->string insertString:valueString atIndex:charIndex];
+	*oldString = (char *) [managerPtr->backup UTF8String];
+    } else {
+	TextManagerClearCache(managerPtr);
+	[managerPtr->string insertString:valueString atIndex:charIndex];
+    }
+    return TextManagerUpdate(managerPtr, numChars, numBytes);
+}
+
+/*
+ *---------------------------------------------------------------------------
+ *
+ *  TkpTextManagerDelete --
+ *
+ *      Delete the number of clusters specified by the count parameter,
+ *      starting at the cluster containing the character referenced by
+ *      the charIndex parameter.  If that character is not a base character
+ *      the entire cluster which contains it will be deleted, so the
+ *      resulting string is a sequence of well-formed clusters.
+ *
+ * Results:
+ *	A pointer to the TextManager's cached UTF-8 string.
+ *
+ * Side effects:
+
+ *      The number of unichars and bytes in the modified NSMutableString are
+ *	recorded in the integer variables referenced by the numChars and
+ *	numBytes parameters.  Also the number of characters which were removed
+ *	is recorded in the integer variable charsDeleted.  The index of each
+ *	remaining base character will change by addition or subtraction of this
+ *	number.  If the parameter charsDeleted is a non-null pointer then it
+ *	will be set to the address of a UTF-8 encoded C string containing the
+ *	deleted characters.  If the parameter oldString is non-NULL then the
+ *	value of the string will be cached before the characters are deleted
+ *	and the pointer referenced by oldString be set to a UTF8-encoded
+ *	C-string representing the cached string.  These strings are meant for
+ *	immediate use in validating the change, and may not persist after the
+ *	next iteration of the event loop.
+ *
+ *---------------------------------------------------------------------------
+ */
+
+const char*
+TkpTextManagerDelete(
+    ClientData clientData,
+    int charIndex,
+    int count,
+    int *numChars,
+    int *numBytes,
+    int *numDeleted,
+    char **charsDeleted,
+    const char **oldString)
+{
+    TextManager *managerPtr = (TextManager *) clientData;
+    NSRange deleteRange = CharRangeFromClusterRange(managerPtr->string,
+						    charIndex, count);
+    if (charsDeleted || oldString) {
+	NSString *diffString = [managerPtr->string
+				   substringWithRange:deleteRange];
+	TextManagerCache(managerPtr);
+	if (charsDeleted) {
+	    *charsDeleted = (char *) [diffString UTF8String];
+	}
+	if (oldString) {
+	    *oldString = [managerPtr->backup UTF8String];
+	}
+    } else {
+	TextManagerClearCache(managerPtr);
+    }
+    [managerPtr->string deleteCharactersInRange: deleteRange];
+    *numDeleted = deleteRange.length;
+    return TextManagerUpdate(managerPtr, numChars, numBytes);
+}
+
+#define GetNSFontTraitsFromTkFontAttributes(faPtr)			\
 	((faPtr)->weight == TK_FW_BOLD ? NSBoldFontMask : NSUnboldFontMask) | \
 	((faPtr)->slant == TK_FS_ITALIC ? NSItalicFontMask : NSUnitalicFontMask)
 
 /*
  *---------------------------------------------------------------------------

Index: tests/entry.test
==================================================================
--- tests/entry.test
+++ tests/entry.test
@@ -982,11 +982,11 @@
     entry .e
 } -body {
     .e in
 } -cleanup {
     destroy .e
-} -returnCodes error -result {ambiguous option "in": must be bbox, cget, configure, delete, get, icursor, index, insert, scan, selection, validate, or xview}
+} -returnCodes error -result {ambiguous option "in": must be bbox, cget, configure, delete, get, icursor, index, insert, range, scan, selection, validate, or xview}
 test entry-3.32 {EntryWidgetCmd procedure, "index" widget command} -setup {
     entry .e
 } -body {
     .e index
 } -cleanup {
@@ -1578,11 +1578,71 @@
     update
 } -body {
     .e gorp
 } -cleanup {
     destroy .e
-} -returnCodes error -result {bad option "gorp": must be bbox, cget, configure, delete, get, icursor, index, insert, scan, selection, validate, or xview}
+} -returnCodes error -result {bad option "gorp": must be bbox, cget, configure, delete, get, icursor, index, insert, range, scan, selection, validate, or xview}
+test entry-3.87 {EntryWidgetCmd procedure, "range" widget command} -setup {
+    entry .e
+    pack .e
+    update
+} -body {
+    .e insert end "01234567890"
+    .e range 1
+} -cleanup {
+    destroy .e
+} -returnCodes 1 -result {wrong # args: should be ".e range first last"}
+test entry-3.88 {EntryWidgetCmd procedure, "range" widget command} -setup {
+    entry .e
+    pack .e
+    update
+} -body {
+    .e insert end "01234567890"
+    .e range 1 5
+} -cleanup {
+    destroy .e
+} -result {12345}
+test entry-3.89 {EntryWidgetCmd procedure, "range" widget command} -setup {
+    entry .e
+    pack .e
+    update
+} -body {
+    .e insert end "01234567890"
+    .e range 5 1
+} -cleanup {
+    destroy .e
+} -result {}
+test entry-3.90 {EntryWidgetCmd procedure, "range" widget command} -setup {
+    entry .e
+    pack .e
+    update
+} -body {
+    .e insert end "01234567890"
+    .e range 5 1
+} -cleanup {
+    destroy .e
+} -result {}
+test entry-3.91 {EntryWidgetCmd procedure, "range" widget command} -setup {
+    entry .e
+    pack .e
+    update
+} -body {
+    .e insert end "01234567890"
+    .e range -10 5
+} -cleanup {
+    destroy .e
+} -result {012345}
+test entry-3.92 {EntryWidgetCmd procedure, "range" widget command} -setup {
+    entry .e
+    pack .e
+    update
+} -body {
+    .e insert end "01234567890"
+    .e range -10 20
+} -cleanup {
+    destroy .e
+} -result {01234567890}
 
 # The test below doesn't actually check anything directly, but if run
 # with Purify or some other memory-allocation-checking program it will
 # ensure that resources get properly freed.