diff options
Diffstat (limited to 'tk8.6/generic/tkText.c')
-rw-r--r-- | tk8.6/generic/tkText.c | 6912 |
1 files changed, 6912 insertions, 0 deletions
diff --git a/tk8.6/generic/tkText.c b/tk8.6/generic/tkText.c new file mode 100644 index 0000000..5ad527a --- /dev/null +++ b/tk8.6/generic/tkText.c @@ -0,0 +1,6912 @@ +/* + * tkText.c -- + * + * This module provides a big chunk of the implementation of multi-line + * editable text widgets for Tk. Among other things, it provides the Tcl + * command interfaces to text widgets. The B-tree representation of text + * and its actual display are implemented elsewhere. + * + * Copyright (c) 1992-1994 The Regents of the University of California. + * Copyright (c) 1994-1996 Sun Microsystems, Inc. + * Copyright (c) 1999 by Scriptics Corporation. + * + * See the file "license.terms" for information on usage and redistribution of + * this file, and for a DISCLAIMER OF ALL WARRANTIES. + */ + +#include "default.h" +#include "tkInt.h" +#include "tkUndo.h" + +#if defined(MAC_OSX_TK) +#define Style TkStyle +#define DInfo TkDInfo +#endif + +/* + * For compatibility with Tk 4.0 through 8.4.x, we allow tabs to be + * mis-specified with non-increasing values. These are converted into tabs + * which are the equivalent of at least a character width apart. + */ + +#if (TK_MAJOR_VERSION < 9) +#define _TK_ALLOW_DECREASING_TABS +#endif + +#include "tkText.h" + +/* + * Used to avoid having to allocate and deallocate arrays on the fly for + * commonly used functions. Must be > 0. + */ + +#define PIXEL_CLIENTS 5 + +/* + * The 'TkTextState' enum in tkText.h is used to define a type for the -state + * option of the Text widget. These values are used as indices into the string + * table below. + */ + +static const char *const stateStrings[] = { + "disabled", "normal", NULL +}; + +/* + * The 'TkWrapMode' enum in tkText.h is used to define a type for the -wrap + * option of the Text widget. These values are used as indices into the string + * table below. + */ + +static const char *const wrapStrings[] = { + "char", "none", "word", NULL +}; + +/* + * The 'TkTextTabStyle' enum in tkText.h is used to define a type for the + * -tabstyle option of the Text widget. These values are used as indices into + * the string table below. + */ + +static const char *const tabStyleStrings[] = { + "tabular", "wordprocessor", NULL +}; + +/* + * The 'TkTextInsertUnfocussed' enum in tkText.h is used to define a type for + * the -insertunfocussed option of the Text widget. These values are used as + * indice into the string table below. + */ + +static const char *const insertUnfocussedStrings[] = { + "hollow", "none", "solid", NULL +}; + +/* + * The following functions and custom option type are used to define the + * "line" option type, and thereby handle the text widget '-startline', + * '-endline' configuration options which are of that type. + * + * We do not need a 'freeProc' because all changes to these two options are + * handled through the TK_TEXT_LINE_RANGE flag in the optionSpecs list, and + * the internal storage is just a pointer, which therefore doesn't need + * freeing. + */ + +static int SetLineStartEnd(ClientData clientData, + Tcl_Interp *interp, Tk_Window tkwin, + Tcl_Obj **value, char *recordPtr, + int internalOffset, char *oldInternalPtr, + int flags); +static Tcl_Obj * GetLineStartEnd(ClientData clientData, + Tk_Window tkwin, char *recordPtr, + int internalOffset); +static void RestoreLineStartEnd(ClientData clientData, + Tk_Window tkwin, char *internalPtr, + char *oldInternalPtr); +static int ObjectIsEmpty(Tcl_Obj *objPtr); + +static const Tk_ObjCustomOption lineOption = { + "line", /* name */ + SetLineStartEnd, /* setProc */ + GetLineStartEnd, /* getProc */ + RestoreLineStartEnd, /* restoreProc */ + NULL, /* freeProc */ + 0 +}; + +/* + * Information used to parse text configuration options: + */ + +static const Tk_OptionSpec optionSpecs[] = { + {TK_OPTION_BOOLEAN, "-autoseparators", "autoSeparators", + "AutoSeparators", DEF_TEXT_AUTO_SEPARATORS, -1, + Tk_Offset(TkText, autoSeparators), + TK_OPTION_DONT_SET_DEFAULT, 0, 0}, + {TK_OPTION_BORDER, "-background", "background", "Background", + DEF_TEXT_BG_COLOR, -1, Tk_Offset(TkText, border), + 0, DEF_TEXT_BG_MONO, 0}, + {TK_OPTION_SYNONYM, "-bd", NULL, NULL, + NULL, 0, -1, 0, "-borderwidth", + TK_TEXT_LINE_GEOMETRY}, + {TK_OPTION_SYNONYM, "-bg", NULL, NULL, + NULL, 0, -1, 0, "-background", 0}, + {TK_OPTION_BOOLEAN, "-blockcursor", "blockCursor", + "BlockCursor", DEF_TEXT_BLOCK_CURSOR, -1, + Tk_Offset(TkText, insertCursorType), 0, 0, 0}, + {TK_OPTION_PIXELS, "-borderwidth", "borderWidth", "BorderWidth", + DEF_TEXT_BORDER_WIDTH, -1, Tk_Offset(TkText, borderWidth), + 0, 0, TK_TEXT_LINE_GEOMETRY}, + {TK_OPTION_CURSOR, "-cursor", "cursor", "Cursor", + DEF_TEXT_CURSOR, -1, Tk_Offset(TkText, cursor), + TK_OPTION_NULL_OK, 0, 0}, + {TK_OPTION_CUSTOM, "-endline", NULL, NULL, + NULL, -1, Tk_Offset(TkText, end), TK_OPTION_NULL_OK, + &lineOption, TK_TEXT_LINE_RANGE}, + {TK_OPTION_BOOLEAN, "-exportselection", "exportSelection", + "ExportSelection", DEF_TEXT_EXPORT_SELECTION, -1, + Tk_Offset(TkText, exportSelection), 0, 0, 0}, + {TK_OPTION_SYNONYM, "-fg", "foreground", NULL, + NULL, 0, -1, 0, "-foreground", 0}, + {TK_OPTION_FONT, "-font", "font", "Font", + DEF_TEXT_FONT, -1, Tk_Offset(TkText, tkfont), 0, 0, + TK_TEXT_LINE_GEOMETRY}, + {TK_OPTION_COLOR, "-foreground", "foreground", "Foreground", + DEF_TEXT_FG, -1, Tk_Offset(TkText, fgColor), 0, + 0, 0}, + {TK_OPTION_PIXELS, "-height", "height", "Height", + DEF_TEXT_HEIGHT, -1, Tk_Offset(TkText, height), 0, 0, 0}, + {TK_OPTION_COLOR, "-highlightbackground", "highlightBackground", + "HighlightBackground", DEF_TEXT_HIGHLIGHT_BG, + -1, Tk_Offset(TkText, highlightBgColorPtr), + 0, 0, 0}, + {TK_OPTION_COLOR, "-highlightcolor", "highlightColor", "HighlightColor", + DEF_TEXT_HIGHLIGHT, -1, Tk_Offset(TkText, highlightColorPtr), + 0, 0, 0}, + {TK_OPTION_PIXELS, "-highlightthickness", "highlightThickness", + "HighlightThickness", DEF_TEXT_HIGHLIGHT_WIDTH, -1, + Tk_Offset(TkText, highlightWidth), 0, 0, TK_TEXT_LINE_GEOMETRY}, + {TK_OPTION_BORDER, "-inactiveselectbackground","inactiveSelectBackground", + "Foreground", + DEF_TEXT_INACTIVE_SELECT_COLOR, + -1, Tk_Offset(TkText, inactiveSelBorder), + TK_OPTION_NULL_OK, DEF_TEXT_SELECT_MONO, 0}, + {TK_OPTION_BORDER, "-insertbackground", "insertBackground", "Foreground", + DEF_TEXT_INSERT_BG, + -1, Tk_Offset(TkText, insertBorder), + 0, 0, 0}, + {TK_OPTION_PIXELS, "-insertborderwidth", "insertBorderWidth", + "BorderWidth", DEF_TEXT_INSERT_BD_COLOR, -1, + Tk_Offset(TkText, insertBorderWidth), 0, + (ClientData) DEF_TEXT_INSERT_BD_MONO, 0}, + {TK_OPTION_INT, "-insertofftime", "insertOffTime", "OffTime", + DEF_TEXT_INSERT_OFF_TIME, -1, Tk_Offset(TkText, insertOffTime), + 0, 0, 0}, + {TK_OPTION_INT, "-insertontime", "insertOnTime", "OnTime", + DEF_TEXT_INSERT_ON_TIME, -1, Tk_Offset(TkText, insertOnTime), + 0, 0, 0}, + {TK_OPTION_STRING_TABLE, + "-insertunfocussed", "insertUnfocussed", "InsertUnfocussed", + DEF_TEXT_INSERT_UNFOCUSSED, -1, Tk_Offset(TkText, insertUnfocussed), + 0, insertUnfocussedStrings, 0}, + {TK_OPTION_PIXELS, "-insertwidth", "insertWidth", "InsertWidth", + DEF_TEXT_INSERT_WIDTH, -1, Tk_Offset(TkText, insertWidth), + 0, 0, 0}, + {TK_OPTION_INT, "-maxundo", "maxUndo", "MaxUndo", + DEF_TEXT_MAX_UNDO, -1, Tk_Offset(TkText, maxUndo), + TK_OPTION_DONT_SET_DEFAULT, 0, 0}, + {TK_OPTION_PIXELS, "-padx", "padX", "Pad", + DEF_TEXT_PADX, -1, Tk_Offset(TkText, padX), 0, 0, + TK_TEXT_LINE_GEOMETRY}, + {TK_OPTION_PIXELS, "-pady", "padY", "Pad", + DEF_TEXT_PADY, -1, Tk_Offset(TkText, padY), 0, 0, 0}, + {TK_OPTION_RELIEF, "-relief", "relief", "Relief", + DEF_TEXT_RELIEF, -1, Tk_Offset(TkText, relief), 0, 0, 0}, + {TK_OPTION_BORDER, "-selectbackground", "selectBackground", "Foreground", + DEF_TEXT_SELECT_COLOR, -1, Tk_Offset(TkText, selBorder), + 0, DEF_TEXT_SELECT_MONO, 0}, + {TK_OPTION_PIXELS, "-selectborderwidth", "selectBorderWidth", + "BorderWidth", DEF_TEXT_SELECT_BD_COLOR, + Tk_Offset(TkText, selBorderWidthPtr), + Tk_Offset(TkText, selBorderWidth), + TK_OPTION_NULL_OK, DEF_TEXT_SELECT_BD_MONO, 0}, + {TK_OPTION_COLOR, "-selectforeground", "selectForeground", "Background", + DEF_TEXT_SELECT_FG_COLOR, -1, Tk_Offset(TkText, selFgColorPtr), + TK_OPTION_NULL_OK, DEF_TEXT_SELECT_FG_MONO, 0}, + {TK_OPTION_BOOLEAN, "-setgrid", "setGrid", "SetGrid", + DEF_TEXT_SET_GRID, -1, Tk_Offset(TkText, setGrid), 0, 0, 0}, + {TK_OPTION_PIXELS, "-spacing1", "spacing1", "Spacing", + DEF_TEXT_SPACING1, -1, Tk_Offset(TkText, spacing1), + 0, 0 , TK_TEXT_LINE_GEOMETRY }, + {TK_OPTION_PIXELS, "-spacing2", "spacing2", "Spacing", + DEF_TEXT_SPACING2, -1, Tk_Offset(TkText, spacing2), + 0, 0 , TK_TEXT_LINE_GEOMETRY }, + {TK_OPTION_PIXELS, "-spacing3", "spacing3", "Spacing", + DEF_TEXT_SPACING3, -1, Tk_Offset(TkText, spacing3), + 0, 0 , TK_TEXT_LINE_GEOMETRY }, + {TK_OPTION_CUSTOM, "-startline", NULL, NULL, + NULL, -1, Tk_Offset(TkText, start), TK_OPTION_NULL_OK, + &lineOption, TK_TEXT_LINE_RANGE}, + {TK_OPTION_STRING_TABLE, "-state", "state", "State", + DEF_TEXT_STATE, -1, Tk_Offset(TkText, state), + 0, stateStrings, 0}, + {TK_OPTION_STRING, "-tabs", "tabs", "Tabs", + DEF_TEXT_TABS, Tk_Offset(TkText, tabOptionPtr), -1, + TK_OPTION_NULL_OK, 0, TK_TEXT_LINE_GEOMETRY}, + {TK_OPTION_STRING_TABLE, "-tabstyle", "tabStyle", "TabStyle", + DEF_TEXT_TABSTYLE, -1, Tk_Offset(TkText, tabStyle), + 0, tabStyleStrings, TK_TEXT_LINE_GEOMETRY}, + {TK_OPTION_STRING, "-takefocus", "takeFocus", "TakeFocus", + DEF_TEXT_TAKE_FOCUS, -1, Tk_Offset(TkText, takeFocus), + TK_OPTION_NULL_OK, 0, 0}, + {TK_OPTION_BOOLEAN, "-undo", "undo", "Undo", + DEF_TEXT_UNDO, -1, Tk_Offset(TkText, undo), + TK_OPTION_DONT_SET_DEFAULT, 0 , 0}, + {TK_OPTION_INT, "-width", "width", "Width", + DEF_TEXT_WIDTH, -1, Tk_Offset(TkText, width), 0, 0, + TK_TEXT_LINE_GEOMETRY}, + {TK_OPTION_STRING_TABLE, "-wrap", "wrap", "Wrap", + DEF_TEXT_WRAP, -1, Tk_Offset(TkText, wrapMode), + 0, wrapStrings, TK_TEXT_LINE_GEOMETRY}, + {TK_OPTION_STRING, "-xscrollcommand", "xScrollCommand", "ScrollCommand", + DEF_TEXT_XSCROLL_COMMAND, -1, Tk_Offset(TkText, xScrollCmd), + TK_OPTION_NULL_OK, 0, 0}, + {TK_OPTION_STRING, "-yscrollcommand", "yScrollCommand", "ScrollCommand", + DEF_TEXT_YSCROLL_COMMAND, -1, Tk_Offset(TkText, yScrollCmd), + TK_OPTION_NULL_OK, 0, 0}, + {TK_OPTION_END, NULL, NULL, NULL, 0, 0, 0, 0, 0, 0} +}; + +/* + * These three typedefs, the structure and the SearchPerform, SearchCore + * functions below are used for line-based searches of the text widget, and, + * in particular, to handle multi-line matching even though the text widget is + * a single-line based data structure. They are completely abstracted away + * from the Text widget internals, however, so could easily be re-used with + * any line-based entity to provide multi-line matching. + * + * We have abstracted this code away from the text widget to try to keep Tk as + * modular as possible. + */ + +struct SearchSpec; /* Forward declaration. */ + +typedef ClientData SearchAddLineProc(int lineNum, + struct SearchSpec *searchSpecPtr, + Tcl_Obj *theLine, int *lenPtr, + int *extraLinesPtr); +typedef int SearchMatchProc(int lineNum, + struct SearchSpec *searchSpecPtr, + ClientData clientData, Tcl_Obj *theLine, + int matchOffset, int matchLength); +typedef int SearchLineIndexProc(Tcl_Interp *interp, + Tcl_Obj *objPtr, struct SearchSpec *searchSpecPtr, + int *linePosPtr, int *offsetPosPtr); + +typedef struct SearchSpec { + int exact; /* Whether search is exact or regexp. */ + int noCase; /* Case-insenstivive? */ + int noLineStop; /* If not set, a regexp search will use the + * TCL_REG_NLSTOP flag. */ + int overlap; /* If set, results from multiple searches + * (-all) are allowed to overlap each + * other. */ + int strictLimits; /* If set, matches must be completely inside + * the from,to range. Otherwise the limits + * only apply to the start of each match. */ + int all; /* Whether all or the first match should be + * reported. */ + int startLine; /* First line to examine. */ + int startOffset; /* Index in first line to start at. */ + int stopLine; /* Last line to examine, or -1 when we search + * all available text. */ + int stopOffset; /* Index to stop at, provided stopLine is not + * -1. */ + int numLines; /* Total lines which are available. */ + int backwards; /* Searching forwards or backwards. */ + Tcl_Obj *varPtr; /* If non-NULL, store length(s) of match(es) + * in this variable. */ + Tcl_Obj *countPtr; /* Keeps track of currently found lengths. */ + Tcl_Obj *resPtr; /* Keeps track of currently found locations */ + int searchElide; /* Search in hidden text as well. */ + SearchAddLineProc *addLineProc; + /* Function to call when we need to add + * another line to the search string so far */ + SearchMatchProc *foundMatchProc; + /* Function to call when we have found a + * match. */ + SearchLineIndexProc *lineIndexProc; + /* Function to call when we have found a + * match. */ + ClientData clientData; /* Information about structure being searched, + * in this case a text widget. */ +} SearchSpec; + +/* + * The text-widget-independent functions which actually perform the search, + * handling both regexp and exact searches. + */ + +static int SearchCore(Tcl_Interp *interp, + SearchSpec *searchSpecPtr, Tcl_Obj *patObj); +static int SearchPerform(Tcl_Interp *interp, + SearchSpec *searchSpecPtr, Tcl_Obj *patObj, + Tcl_Obj *fromPtr, Tcl_Obj *toPtr); + +/* + * Boolean variable indicating whether or not special debugging code should be + * executed. + */ + +int tkTextDebug = 0; + +/* + * Forward declarations for functions defined later in this file: + */ + +static int ConfigureText(Tcl_Interp *interp, + TkText *textPtr, int objc, Tcl_Obj *const objv[]); +static int DeleteIndexRange(TkSharedText *sharedPtr, + TkText *textPtr, const TkTextIndex *indexPtr1, + const TkTextIndex *indexPtr2, int viewUpdate); +static int CountIndices(const TkText *textPtr, + const TkTextIndex *indexPtr1, + const TkTextIndex *indexPtr2, + TkTextCountType type); +static void DestroyText(TkText *textPtr); +static int InsertChars(TkSharedText *sharedTextPtr, + TkText *textPtr, TkTextIndex *indexPtr, + Tcl_Obj *stringPtr, int viewUpdate); +static void TextBlinkProc(ClientData clientData); +static void TextCmdDeletedProc(ClientData clientData); +static int CreateWidget(TkSharedText *sharedPtr, Tk_Window tkwin, + Tcl_Interp *interp, const TkText *parent, + int objc, Tcl_Obj *const objv[]); +static void TextEventProc(ClientData clientData, + XEvent *eventPtr); +static int TextFetchSelection(ClientData clientData, int offset, + char *buffer, int maxBytes); +static int TextIndexSortProc(const void *first, + const void *second); +static int TextInsertCmd(TkSharedText *sharedTextPtr, + TkText *textPtr, Tcl_Interp *interp, + int objc, Tcl_Obj *const objv[], + const TkTextIndex *indexPtr, int viewUpdate); +static int TextReplaceCmd(TkText *textPtr, Tcl_Interp *interp, + const TkTextIndex *indexFromPtr, + const TkTextIndex *indexToPtr, + int objc, Tcl_Obj *const objv[], int viewUpdate); +static int TextSearchCmd(TkText *textPtr, Tcl_Interp *interp, + int objc, Tcl_Obj *const objv[]); +static int TextEditCmd(TkText *textPtr, Tcl_Interp *interp, + int objc, Tcl_Obj *const objv[]); +static int TextWidgetObjCmd(ClientData clientData, + Tcl_Interp *interp, + int objc, Tcl_Obj *const objv[]); +static int SharedTextObjCmd(ClientData clientData, + Tcl_Interp *interp, + int objc, Tcl_Obj *const objv[]); +static void TextWorldChangedCallback(ClientData instanceData); +static void TextWorldChanged(TkText *textPtr, int mask); +static int TextDumpCmd(TkText *textPtr, Tcl_Interp *interp, + int objc, Tcl_Obj *const objv[]); +static int DumpLine(Tcl_Interp *interp, TkText *textPtr, + int what, TkTextLine *linePtr, int start, int end, + int lineno, Tcl_Obj *command); +static int DumpSegment(TkText *textPtr, Tcl_Interp *interp, + const char *key, const char *value, + Tcl_Obj *command, const TkTextIndex *index, + int what); +static int TextEditUndo(TkText *textPtr); +static int TextEditRedo(TkText *textPtr); +static Tcl_Obj * TextGetText(const TkText *textPtr, + const TkTextIndex *index1, + const TkTextIndex *index2, int visibleOnly); +static void GenerateModifiedEvent(TkText *textPtr); +static void GenerateUndoStackEvent(TkText *textPtr); +static void UpdateDirtyFlag(TkSharedText *sharedPtr); +static void RunAfterSyncCmd(ClientData clientData); +static void TextPushUndoAction(TkText *textPtr, + Tcl_Obj *undoString, int insert, + const TkTextIndex *index1Ptr, + const TkTextIndex *index2Ptr); +static int TextSearchIndexInLine(const SearchSpec *searchSpecPtr, + TkTextLine *linePtr, int byteIndex); +static int TextPeerCmd(TkText *textPtr, Tcl_Interp *interp, + int objc, Tcl_Obj *const objv[]); +static TkUndoProc TextUndoRedoCallback; + +/* + * Declarations of the three search procs required by the multi-line search + * routines. + */ + +static SearchMatchProc TextSearchFoundMatch; +static SearchAddLineProc TextSearchAddNextLine; +static SearchLineIndexProc TextSearchGetLineIndex; + +/* + * The structure below defines text class behavior by means of functions that + * can be invoked from generic window code. + */ + +static const Tk_ClassProcs textClass = { + sizeof(Tk_ClassProcs), /* size */ + TextWorldChangedCallback, /* worldChangedProc */ + NULL, /* createProc */ + NULL /* modalProc */ +}; + +/* + *-------------------------------------------------------------- + * + * Tk_TextObjCmd -- + * + * This function is invoked to process the "text" Tcl command. See the + * user documentation for details on what it does. + * + * Results: + * A standard Tcl result. + * + * Side effects: + * See the user documentation. + * + *-------------------------------------------------------------- + */ + +int +Tk_TextObjCmd( + ClientData clientData, /* Main window associated with interpreter. */ + Tcl_Interp *interp, /* Current interpreter. */ + int objc, /* Number of arguments. */ + Tcl_Obj *const objv[]) /* Argument objects. */ +{ + Tk_Window tkwin = clientData; + + if (objc < 2) { + Tcl_WrongNumArgs(interp, 1, objv, "pathName ?-option value ...?"); + return TCL_ERROR; + } + + return CreateWidget(NULL, tkwin, interp, NULL, objc, objv); +} + +/* + *-------------------------------------------------------------- + * + * CreateWidget -- + * + * This function is invoked to process the "text" Tcl command, (when + * called by Tk_TextObjCmd) and the "$text peer create" text widget + * sub-command (called from TextPeerCmd). + * + * See the user documentation for details on what it does. + * + * Results: + * A standard Tcl result, places the name of the widget created into the + * interp's result. + * + * Side effects: + * See the user documentation. + * + *-------------------------------------------------------------- + */ + +static int +CreateWidget( + TkSharedText *sharedPtr, /* Shared widget info, or NULL. */ + Tk_Window tkwin, /* Main window associated with interpreter. */ + Tcl_Interp *interp, /* Current interpreter. */ + const TkText *parent, /* If non-NULL then take default start, end + * from this parent. */ + int objc, /* Number of arguments. */ + Tcl_Obj *const objv[]) /* Argument objects. */ +{ + register TkText *textPtr; + Tk_OptionTable optionTable; + TkTextIndex startIndex; + Tk_Window newWin; + + /* + * Create the window. + */ + + newWin = Tk_CreateWindowFromPath(interp, tkwin, Tcl_GetString(objv[1]), + NULL); + if (newWin == NULL) { + return TCL_ERROR; + } + + /* + * Create the text widget and initialize everything to zero, then set the + * necessary initial (non-NULL) values. It is important that the 'set' tag + * and 'insert', 'current' mark pointers are all NULL to start. + */ + + textPtr = ckalloc(sizeof(TkText)); + memset(textPtr, 0, sizeof(TkText)); + + textPtr->tkwin = newWin; + textPtr->display = Tk_Display(newWin); + textPtr->interp = interp; + textPtr->widgetCmd = Tcl_CreateObjCommand(interp, + Tk_PathName(textPtr->tkwin), TextWidgetObjCmd, + textPtr, TextCmdDeletedProc); + + if (sharedPtr == NULL) { + sharedPtr = ckalloc(sizeof(TkSharedText)); + memset(sharedPtr, 0, sizeof(TkSharedText)); + + sharedPtr->refCount = 0; + sharedPtr->peers = NULL; + sharedPtr->tree = TkBTreeCreate(sharedPtr); + + Tcl_InitHashTable(&sharedPtr->tagTable, TCL_STRING_KEYS); + Tcl_InitHashTable(&sharedPtr->markTable, TCL_STRING_KEYS); + Tcl_InitHashTable(&sharedPtr->windowTable, TCL_STRING_KEYS); + Tcl_InitHashTable(&sharedPtr->imageTable, TCL_STRING_KEYS); + sharedPtr->undoStack = TkUndoInitStack(interp,0); + sharedPtr->undo = 1; + sharedPtr->isDirty = 0; + sharedPtr->dirtyMode = TK_TEXT_DIRTY_NORMAL; + sharedPtr->autoSeparators = 1; + sharedPtr->lastEditMode = TK_TEXT_EDIT_OTHER; + sharedPtr->stateEpoch = 0; + } + + /* + * Add the new widget to the shared list. + */ + + textPtr->sharedTextPtr = sharedPtr; + sharedPtr->refCount++; + textPtr->next = sharedPtr->peers; + sharedPtr->peers = textPtr; + + /* + * This refCount will be held until DestroyText is called. Note also that + * the later call to 'TkTextCreateDInfo' will add more refCounts. + */ + + textPtr->refCount = 1; + + /* + * Specify start and end lines in the B-tree. The default is the same as + * the parent, but this can be adjusted to display more or less if the + * start, end where given as configuration options. + */ + + if (parent != NULL) { + textPtr->start = parent->start; + textPtr->end = parent->end; + } else { + textPtr->start = NULL; + textPtr->end = NULL; + } + + textPtr->state = TK_TEXT_STATE_NORMAL; + textPtr->relief = TK_RELIEF_FLAT; + textPtr->cursor = None; + textPtr->charWidth = 1; + textPtr->charHeight = 10; + textPtr->wrapMode = TEXT_WRAPMODE_CHAR; + textPtr->prevWidth = Tk_Width(newWin); + textPtr->prevHeight = Tk_Height(newWin); + + /* + * Register with the B-tree. In some sense it would be best if we could do + * this later (after configuration options), so that any changes to + * start,end do not require a total recalculation. + */ + + TkBTreeAddClient(sharedPtr->tree, textPtr, textPtr->charHeight); + + /* + * This will add refCounts to textPtr. + */ + + TkTextCreateDInfo(textPtr); + TkTextMakeByteIndex(textPtr->sharedTextPtr->tree, textPtr, 0, 0, + &startIndex); + TkTextSetYView(textPtr, &startIndex, 0); + textPtr->exportSelection = 1; + textPtr->pickEvent.type = LeaveNotify; + textPtr->undo = textPtr->sharedTextPtr->undo; + textPtr->maxUndo = textPtr->sharedTextPtr->maxUndo; + textPtr->autoSeparators = textPtr->sharedTextPtr->autoSeparators; + textPtr->tabOptionPtr = NULL; + + /* + * Create the "sel" tag and the "current" and "insert" marks. + */ + + textPtr->selBorder = NULL; + textPtr->inactiveSelBorder = NULL; + textPtr->selBorderWidth = 0; + textPtr->selBorderWidthPtr = NULL; + textPtr->selFgColorPtr = NULL; + + /* + * Note: it is important that textPtr->selTagPtr is NULL before this + * initial call. + */ + + textPtr->selTagPtr = TkTextCreateTag(textPtr, "sel", NULL); + textPtr->selTagPtr->reliefString = + ckalloc(sizeof(DEF_TEXT_SELECT_RELIEF)); + strcpy(textPtr->selTagPtr->reliefString, DEF_TEXT_SELECT_RELIEF); + Tk_GetRelief(interp, DEF_TEXT_SELECT_RELIEF, &textPtr->selTagPtr->relief); + textPtr->currentMarkPtr = TkTextSetMark(textPtr, "current", &startIndex); + textPtr->insertMarkPtr = TkTextSetMark(textPtr, "insert", &startIndex); + + /* + * Create the option table for this widget class. If it has already been + * created, the cached pointer will be returned. + */ + + optionTable = Tk_CreateOptionTable(interp, optionSpecs); + + Tk_SetClass(textPtr->tkwin, "Text"); + Tk_SetClassProcs(textPtr->tkwin, &textClass, textPtr); + textPtr->optionTable = optionTable; + + Tk_CreateEventHandler(textPtr->tkwin, + ExposureMask|StructureNotifyMask|FocusChangeMask, + TextEventProc, textPtr); + Tk_CreateEventHandler(textPtr->tkwin, KeyPressMask|KeyReleaseMask + |ButtonPressMask|ButtonReleaseMask|EnterWindowMask + |LeaveWindowMask|PointerMotionMask|VirtualEventMask, + TkTextBindProc, textPtr); + Tk_CreateSelHandler(textPtr->tkwin, XA_PRIMARY, XA_STRING, + TextFetchSelection, textPtr, XA_STRING); + + if (Tk_InitOptions(interp, (char *) textPtr, optionTable, textPtr->tkwin) + != TCL_OK) { + Tk_DestroyWindow(textPtr->tkwin); + return TCL_ERROR; + } + if (ConfigureText(interp, textPtr, objc-2, objv+2) != TCL_OK) { + Tk_DestroyWindow(textPtr->tkwin); + return TCL_ERROR; + } + + Tcl_SetObjResult(interp, TkNewWindowObj(textPtr->tkwin)); + return TCL_OK; +} + +/* + *-------------------------------------------------------------- + * + * TextWidgetObjCmd -- + * + * This function is invoked to process the Tcl command that corresponds + * to a text widget. See the user documentation for details on what it + * does. + * + * Results: + * A standard Tcl result. + * + * Side effects: + * See the user documentation. + * + *-------------------------------------------------------------- + */ + +static int +TextWidgetObjCmd( + ClientData clientData, /* Information about text widget. */ + Tcl_Interp *interp, /* Current interpreter. */ + int objc, /* Number of arguments. */ + Tcl_Obj *const objv[]) /* Argument objects. */ +{ + register TkText *textPtr = clientData; + int result = TCL_OK; + int index; + + static const char *const optionStrings[] = { + "bbox", "cget", "compare", "configure", "count", "debug", "delete", + "dlineinfo", "dump", "edit", "get", "image", "index", "insert", + "mark", "peer", "pendingsync", "replace", "scan", "search", + "see", "sync", "tag", "window", "xview", "yview", NULL + }; + enum options { + TEXT_BBOX, TEXT_CGET, TEXT_COMPARE, TEXT_CONFIGURE, TEXT_COUNT, + TEXT_DEBUG, TEXT_DELETE, TEXT_DLINEINFO, TEXT_DUMP, TEXT_EDIT, + TEXT_GET, TEXT_IMAGE, TEXT_INDEX, TEXT_INSERT, TEXT_MARK, + TEXT_PEER, TEXT_PENDINGSYNC, TEXT_REPLACE, TEXT_SCAN, + TEXT_SEARCH, TEXT_SEE, TEXT_SYNC, TEXT_TAG, TEXT_WINDOW, + TEXT_XVIEW, TEXT_YVIEW + }; + + if (objc < 2) { + Tcl_WrongNumArgs(interp, 1, objv, "option ?arg ...?"); + return TCL_ERROR; + } + + if (Tcl_GetIndexFromObjStruct(interp, objv[1], optionStrings, + sizeof(char *), "option", 0, &index) != TCL_OK) { + return TCL_ERROR; + } + textPtr->refCount++; + + switch ((enum options) index) { + case TEXT_BBOX: { + int x, y, width, height; + const TkTextIndex *indexPtr; + + if (objc != 3) { + Tcl_WrongNumArgs(interp, 2, objv, "index"); + result = TCL_ERROR; + goto done; + } + indexPtr = TkTextGetIndexFromObj(interp, textPtr, objv[2]); + if (indexPtr == NULL) { + result = TCL_ERROR; + goto done; + } + if (TkTextIndexBbox(textPtr, indexPtr, &x, &y, &width, &height, + NULL) == 0) { + Tcl_Obj *listObj = Tcl_NewListObj(0, NULL); + + Tcl_ListObjAppendElement(interp, listObj, Tcl_NewIntObj(x)); + Tcl_ListObjAppendElement(interp, listObj, Tcl_NewIntObj(y)); + Tcl_ListObjAppendElement(interp, listObj, Tcl_NewIntObj(width)); + Tcl_ListObjAppendElement(interp, listObj, Tcl_NewIntObj(height)); + + Tcl_SetObjResult(interp, listObj); + } + break; + } + case TEXT_CGET: + if (objc != 3) { + Tcl_WrongNumArgs(interp, 2, objv, "option"); + result = TCL_ERROR; + goto done; + } else { + Tcl_Obj *objPtr = Tk_GetOptionValue(interp, (char *) textPtr, + textPtr->optionTable, objv[2], textPtr->tkwin); + + if (objPtr == NULL) { + result = TCL_ERROR; + goto done; + } + Tcl_SetObjResult(interp, objPtr); + result = TCL_OK; + } + break; + case TEXT_COMPARE: { + int relation, value; + const char *p; + const TkTextIndex *index1Ptr, *index2Ptr; + + if (objc != 5) { + Tcl_WrongNumArgs(interp, 2, objv, "index1 op index2"); + result = TCL_ERROR; + goto done; + } + index1Ptr = TkTextGetIndexFromObj(interp, textPtr, objv[2]); + index2Ptr = TkTextGetIndexFromObj(interp, textPtr, objv[4]); + if (index1Ptr == NULL || index2Ptr == NULL) { + result = TCL_ERROR; + goto done; + } + relation = TkTextIndexCmp(index1Ptr, index2Ptr); + p = Tcl_GetString(objv[3]); + if (p[0] == '<') { + value = (relation < 0); + if ((p[1] == '=') && (p[2] == 0)) { + value = (relation <= 0); + } else if (p[1] != 0) { + goto compareError; + } + } else if (p[0] == '>') { + value = (relation > 0); + if ((p[1] == '=') && (p[2] == 0)) { + value = (relation >= 0); + } else if (p[1] != 0) { + goto compareError; + } + } else if ((p[0] == '=') && (p[1] == '=') && (p[2] == 0)) { + value = (relation == 0); + } else if ((p[0] == '!') && (p[1] == '=') && (p[2] == 0)) { + value = (relation != 0); + } else { + goto compareError; + } + Tcl_SetObjResult(interp, Tcl_NewBooleanObj(value)); + break; + + compareError: + Tcl_SetObjResult(interp, Tcl_ObjPrintf( + "bad comparison operator \"%s\": must be" + " <, <=, ==, >=, >, or !=", Tcl_GetString(objv[3]))); + Tcl_SetErrorCode(interp, "TK", "VALUE", "COMPARISON", NULL); + result = TCL_ERROR; + goto done; + } + case TEXT_CONFIGURE: + if (objc <= 3) { + Tcl_Obj *objPtr = Tk_GetOptionInfo(interp, (char *) textPtr, + textPtr->optionTable, ((objc == 3) ? objv[2] : NULL), + textPtr->tkwin); + + if (objPtr == NULL) { + result = TCL_ERROR; + goto done; + } + Tcl_SetObjResult(interp, objPtr); + } else { + result = ConfigureText(interp, textPtr, objc-2, objv+2); + } + break; + case TEXT_COUNT: { + const TkTextIndex *indexFromPtr, *indexToPtr; + int i, found = 0, update = 0; + Tcl_Obj *objPtr = NULL; + + if (objc < 4) { + Tcl_WrongNumArgs(interp, 2, objv, + "?-option value ...? index1 index2"); + result = TCL_ERROR; + goto done; + } + + indexFromPtr = TkTextGetIndexFromObj(interp, textPtr, objv[objc-2]); + if (indexFromPtr == NULL) { + result = TCL_ERROR; + goto done; + } + indexToPtr = TkTextGetIndexFromObj(interp, textPtr, objv[objc-1]); + if (indexToPtr == NULL) { + result = TCL_ERROR; + goto done; + } + + for (i = 2; i < objc-2; i++) { + int value, length; + const char *option = Tcl_GetString(objv[i]); + char c; + + length = objv[i]->length; + if (length < 2 || option[0] != '-') { + goto badOption; + } + c = option[1]; + if (c == 'c' && !strncmp("-chars", option, (unsigned) length)) { + value = CountIndices(textPtr, indexFromPtr, indexToPtr, + COUNT_CHARS); + } else if (c == 'd' && (length > 8) + && !strncmp("-displaychars", option, (unsigned) length)) { + value = CountIndices(textPtr, indexFromPtr, indexToPtr, + COUNT_DISPLAY_CHARS); + } else if (c == 'd' && (length > 8) + && !strncmp("-displayindices", option,(unsigned)length)) { + value = CountIndices(textPtr, indexFromPtr, indexToPtr, + COUNT_DISPLAY_INDICES); + } else if (c == 'd' && (length > 8) + && !strncmp("-displaylines", option, (unsigned) length)) { + TkTextLine *fromPtr, *lastPtr; + TkTextIndex index, index2; + + int compare = TkTextIndexCmp(indexFromPtr, indexToPtr); + value = 0; + + if (compare == 0) { + goto countDone; + } + + if (compare > 0) { + const TkTextIndex *tmpPtr = indexFromPtr; + + indexFromPtr = indexToPtr; + indexToPtr = tmpPtr; + } + + lastPtr = TkBTreeFindLine(textPtr->sharedTextPtr->tree, + textPtr, + TkBTreeNumLines(textPtr->sharedTextPtr->tree,textPtr)); + fromPtr = indexFromPtr->linePtr; + if (fromPtr == lastPtr) { + goto countDone; + } + + /* + * Caution: we must NEVER call TkTextUpdateOneLine with the + * last artificial line in the widget. + */ + + index = *indexFromPtr; + index.byteIndex = 0; + + /* + * We're going to count up all display lines in the logical + * line of 'indexFromPtr' up to, but not including the logical + * line of 'indexToPtr' (except if this line is elided), and + * then subtract off what came in too much from elided lines, + * also subtract off what we didn't want from 'from' and add + * on what we didn't count from 'to'. + */ + + while (TkTextIndexCmp(&index,indexToPtr) < 0) { + value += TkTextUpdateOneLine(textPtr, index.linePtr, + 0, &index, 0); + } + + index2 = index; + + /* + * Now we need to adjust the count to: + * - subtract off the number of display lines between + * indexToPtr and index2, since we might have skipped past + * indexToPtr, if we have several logical lines in a + * single display line + * - subtract off the number of display lines overcounted + * in the first logical line + * - add on the number of display lines in the last logical + * line + * This logic is still ok if both indexFromPtr and indexToPtr + * are in the same logical line. + */ + + index = *indexToPtr; + index.byteIndex = 0; + while (TkTextIndexCmp(&index,&index2) < 0) { + value -= TkTextUpdateOneLine(textPtr, index.linePtr, + 0, &index, 0); + } + index.linePtr = indexFromPtr->linePtr; + index.byteIndex = 0; + while (1) { + TkTextFindDisplayLineEnd(textPtr, &index, 1, NULL); + if (TkTextIndexCmp(&index,indexFromPtr) >= 0) { + break; + } + TkTextIndexForwBytes(textPtr, &index, 1, &index); + value--; + + } + if (indexToPtr->linePtr != lastPtr) { + index.linePtr = indexToPtr->linePtr; + index.byteIndex = 0; + while (1) { + TkTextFindDisplayLineEnd(textPtr, &index, 1, NULL); + if (TkTextIndexCmp(&index,indexToPtr) >= 0) { + break; + } + TkTextIndexForwBytes(textPtr, &index, 1, &index); + value++; + } + } + + if (compare > 0) { + value = -value; + } + } else if (c == 'i' + && !strncmp("-indices", option, (unsigned) length)) { + value = CountIndices(textPtr, indexFromPtr, indexToPtr, + COUNT_INDICES); + } else if (c == 'l' + && !strncmp("-lines", option, (unsigned) length)) { + value = TkBTreeLinesTo(textPtr, indexToPtr->linePtr) + - TkBTreeLinesTo(textPtr, indexFromPtr->linePtr); + } else if (c == 'u' + && !strncmp("-update", option, (unsigned) length)) { + update = 1; + continue; + } else if (c == 'x' + && !strncmp("-xpixels", option, (unsigned) length)) { + int x1, x2; + TkTextIndex index; + + index = *indexFromPtr; + TkTextFindDisplayLineEnd(textPtr, &index, 0, &x1); + index = *indexToPtr; + TkTextFindDisplayLineEnd(textPtr, &index, 0, &x2); + value = x2 - x1; + } else if (c == 'y' + && !strncmp("-ypixels", option, (unsigned) length)) { + if (update) { + TkTextUpdateLineMetrics(textPtr, + TkBTreeLinesTo(textPtr, indexFromPtr->linePtr), + TkBTreeLinesTo(textPtr, indexToPtr->linePtr), -1); + } + value = TkTextIndexYPixels(textPtr, indexToPtr) + - TkTextIndexYPixels(textPtr, indexFromPtr); + } else { + goto badOption; + } + + countDone: + found++; + if (found == 1) { + Tcl_SetObjResult(interp, Tcl_NewIntObj(value)); + } else { + if (found == 2) { + /* + * Move the first item we put into the result into the + * first element of the list object. + */ + + objPtr = Tcl_NewObj(); + Tcl_ListObjAppendElement(NULL, objPtr, + Tcl_GetObjResult(interp)); + } + Tcl_ListObjAppendElement(NULL, objPtr, Tcl_NewIntObj(value)); + } + } + + if (found == 0) { + /* + * Use the default '-indices'. + */ + + int value = CountIndices(textPtr, indexFromPtr, indexToPtr, + COUNT_INDICES); + + Tcl_SetObjResult(interp, Tcl_NewIntObj(value)); + } else if (found > 1) { + Tcl_SetObjResult(interp, objPtr); + } + break; + + badOption: + Tcl_SetObjResult(interp, Tcl_ObjPrintf( + "bad option \"%s\" must be -chars, -displaychars, " + "-displayindices, -displaylines, -indices, -lines, -update, " + "-xpixels, or -ypixels", Tcl_GetString(objv[i]))); + Tcl_SetErrorCode(interp, "TK", "TEXT", "INDEX_OPTION", NULL); + result = TCL_ERROR; + goto done; + } + case TEXT_DEBUG: + if (objc > 3) { + Tcl_WrongNumArgs(interp, 2, objv, "boolean"); + result = TCL_ERROR; + goto done; + } + if (objc == 2) { + Tcl_SetObjResult(interp, Tcl_NewBooleanObj(tkBTreeDebug)); + } else { + if (Tcl_GetBooleanFromObj(interp, objv[2], + &tkBTreeDebug) != TCL_OK) { + result = TCL_ERROR; + goto done; + } + tkTextDebug = tkBTreeDebug; + } + break; + case TEXT_DELETE: + if (objc < 3) { + Tcl_WrongNumArgs(interp, 2, objv, "index1 ?index2 ...?"); + result = TCL_ERROR; + goto done; + } + if (textPtr->state == TK_TEXT_STATE_NORMAL) { + if (objc < 5) { + /* + * Simple case requires no predetermination of indices. + */ + + const TkTextIndex *indexPtr1, *indexPtr2; + + /* + * Parse the starting and stopping indices. + */ + + indexPtr1 = TkTextGetIndexFromObj(textPtr->interp, textPtr, + objv[2]); + if (indexPtr1 == NULL) { + result = TCL_ERROR; + goto done; + } + if (objc == 4) { + indexPtr2 = TkTextGetIndexFromObj(textPtr->interp, + textPtr, objv[3]); + if (indexPtr2 == NULL) { + result = TCL_ERROR; + goto done; + } + } else { + indexPtr2 = NULL; + } + DeleteIndexRange(NULL, textPtr, indexPtr1, indexPtr2, 1); + } else { + /* + * Multi-index pair case requires that we prevalidate the + * indices and sort from last to first so that deletes occur + * in the exact (unshifted) text. It also needs to handle + * partial and fully overlapping ranges. We have to do this + * with multiple passes. + */ + + TkTextIndex *indices, *ixStart, *ixEnd, *lastStart; + char *useIdx; + int i; + + objc -= 2; + objv += 2; + indices = ckalloc((objc + 1) * sizeof(TkTextIndex)); + + /* + * First pass verifies that all indices are valid. + */ + + for (i = 0; i < objc; i++) { + const TkTextIndex *indexPtr = + TkTextGetIndexFromObj(interp, textPtr, objv[i]); + + if (indexPtr == NULL) { + result = TCL_ERROR; + ckfree(indices); + goto done; + } + indices[i] = *indexPtr; + } + + /* + * Pad out the pairs evenly to make later code easier. + */ + + if (objc & 1) { + indices[i] = indices[i-1]; + TkTextIndexForwChars(NULL, &indices[i], 1, &indices[i], + COUNT_INDICES); + objc++; + } + useIdx = ckalloc(objc); + memset(useIdx, 0, (unsigned) objc); + + /* + * Do a decreasing order sort so that we delete the end ranges + * first to maintain index consistency. + */ + + qsort(indices, (unsigned) objc / 2, + 2 * sizeof(TkTextIndex), TextIndexSortProc); + lastStart = NULL; + + /* + * Second pass will handle bogus ranges (end < start) and + * overlapping ranges. + */ + + for (i = 0; i < objc; i += 2) { + ixStart = &indices[i]; + ixEnd = &indices[i+1]; + if (TkTextIndexCmp(ixEnd, ixStart) <= 0) { + continue; + } + if (lastStart) { + if (TkTextIndexCmp(ixStart, lastStart) == 0) { + /* + * Start indices were equal, and the sort placed + * the longest range first, so skip this one. + */ + + continue; + } else if (TkTextIndexCmp(lastStart, ixEnd) < 0) { + /* + * The next pair has a start range before the end + * point of the last range. Constrain the delete + * range, but use the pointer values. + */ + + *ixEnd = *lastStart; + if (TkTextIndexCmp(ixEnd, ixStart) <= 0) { + continue; + } + } + } + lastStart = ixStart; + useIdx[i] = 1; + } + + /* + * Final pass take the input from the previous and deletes the + * ranges which are flagged to be deleted. + */ + + for (i = 0; i < objc; i += 2) { + if (useIdx[i]) { + /* + * We don't need to check the return value because all + * indices are preparsed above. + */ + + DeleteIndexRange(NULL, textPtr, &indices[i], + &indices[i+1], 1); + } + } + ckfree(indices); + } + } + break; + case TEXT_DLINEINFO: { + int x, y, width, height, base; + const TkTextIndex *indexPtr; + + if (objc != 3) { + Tcl_WrongNumArgs(interp, 2, objv, "index"); + result = TCL_ERROR; + goto done; + } + indexPtr = TkTextGetIndexFromObj(interp, textPtr, objv[2]); + if (indexPtr == NULL) { + result = TCL_ERROR; + goto done; + } + if (TkTextDLineInfo(textPtr, indexPtr, &x, &y, &width, &height, + &base) == 0) { + Tcl_Obj *listObj = Tcl_NewListObj(0, NULL); + + Tcl_ListObjAppendElement(interp, listObj, Tcl_NewIntObj(x)); + Tcl_ListObjAppendElement(interp, listObj, Tcl_NewIntObj(y)); + Tcl_ListObjAppendElement(interp, listObj, Tcl_NewIntObj(width)); + Tcl_ListObjAppendElement(interp, listObj, Tcl_NewIntObj(height)); + Tcl_ListObjAppendElement(interp, listObj, Tcl_NewIntObj(base)); + + Tcl_SetObjResult(interp, listObj); + } + break; + } + case TEXT_DUMP: + result = TextDumpCmd(textPtr, interp, objc, objv); + break; + case TEXT_EDIT: + result = TextEditCmd(textPtr, interp, objc, objv); + break; + case TEXT_GET: { + Tcl_Obj *objPtr = NULL; + int i, found = 0, visible = 0; + const char *name; + int length; + + if (objc < 3) { + Tcl_WrongNumArgs(interp, 2, objv, + "?-displaychars? ?--? index1 ?index2 ...?"); + result = TCL_ERROR; + goto done; + } + + /* + * Simple, restrictive argument parsing. The only options are -- and + * -displaychars (or any unique prefix). + */ + + i = 2; + if (objc > 3) { + name = Tcl_GetString(objv[i]); + length = objv[i]->length; + if (length > 1 && name[0] == '-') { + if (strncmp("-displaychars", name, (unsigned) length) == 0) { + i++; + visible = 1; + name = Tcl_GetString(objv[i]); + length = objv[i]->length; + } + if ((i < objc-1) && (length == 2) && !strcmp("--", name)) { + i++; + } + } + } + + for (; i < objc; i += 2) { + const TkTextIndex *index1Ptr, *index2Ptr; + TkTextIndex index2; + + index1Ptr = TkTextGetIndexFromObj(interp, textPtr, objv[i]); + if (index1Ptr == NULL) { + if (objPtr) { + Tcl_DecrRefCount(objPtr); + } + result = TCL_ERROR; + goto done; + } + + if (i+1 == objc) { + TkTextIndexForwChars(NULL, index1Ptr, 1, &index2, + COUNT_INDICES); + index2Ptr = &index2; + } else { + index2Ptr = TkTextGetIndexFromObj(interp, textPtr, objv[i+1]); + if (index2Ptr == NULL) { + if (objPtr) { + Tcl_DecrRefCount(objPtr); + } + result = TCL_ERROR; + goto done; + } + } + + if (TkTextIndexCmp(index1Ptr, index2Ptr) < 0) { + /* + * We want to move the text we get from the window into the + * result, but since this could in principle be a megabyte or + * more, we want to do it efficiently! + */ + + Tcl_Obj *get = TextGetText(textPtr, index1Ptr, index2Ptr, + visible); + + found++; + if (found == 1) { + Tcl_SetObjResult(interp, get); + } else { + if (found == 2) { + /* + * Move the first item we put into the result into the + * first element of the list object. + */ + + objPtr = Tcl_NewObj(); + Tcl_ListObjAppendElement(NULL, objPtr, + Tcl_GetObjResult(interp)); + } + Tcl_ListObjAppendElement(NULL, objPtr, get); + } + } + } + if (found > 1) { + Tcl_SetObjResult(interp, objPtr); + } + break; + } + case TEXT_IMAGE: + result = TkTextImageCmd(textPtr, interp, objc, objv); + break; + case TEXT_INDEX: { + const TkTextIndex *indexPtr; + + if (objc != 3) { + Tcl_WrongNumArgs(interp, 2, objv, "index"); + result = TCL_ERROR; + goto done; + } + + indexPtr = TkTextGetIndexFromObj(interp, textPtr, objv[2]); + if (indexPtr == NULL) { + result = TCL_ERROR; + goto done; + } + Tcl_SetObjResult(interp, TkTextNewIndexObj(textPtr, indexPtr)); + break; + } + case TEXT_INSERT: { + const TkTextIndex *indexPtr; + + if (objc < 4) { + Tcl_WrongNumArgs(interp, 2, objv, + "index chars ?tagList chars tagList ...?"); + result = TCL_ERROR; + goto done; + } + indexPtr = TkTextGetIndexFromObj(interp, textPtr, objv[2]); + if (indexPtr == NULL) { + result = TCL_ERROR; + goto done; + } + if (textPtr->state == TK_TEXT_STATE_NORMAL) { + result = TextInsertCmd(NULL, textPtr, interp, objc-3, objv+3, + indexPtr, 1); + } + break; + } + case TEXT_MARK: + result = TkTextMarkCmd(textPtr, interp, objc, objv); + break; + case TEXT_PEER: + result = TextPeerCmd(textPtr, interp, objc, objv); + break; + case TEXT_PENDINGSYNC: { + if (objc != 2) { + Tcl_WrongNumArgs(interp, 2, objv, NULL); + result = TCL_ERROR; + goto done; + } + Tcl_SetObjResult(interp, + Tcl_NewBooleanObj(TkTextPendingsync(textPtr))); + break; + } + case TEXT_REPLACE: { + const TkTextIndex *indexFromPtr, *indexToPtr; + + if (objc < 5) { + Tcl_WrongNumArgs(interp, 2, objv, + "index1 index2 chars ?tagList chars tagList ...?"); + result = TCL_ERROR; + goto done; + } + indexFromPtr = TkTextGetIndexFromObj(interp, textPtr, objv[2]); + if (indexFromPtr == NULL) { + result = TCL_ERROR; + goto done; + } + indexToPtr = TkTextGetIndexFromObj(interp, textPtr, objv[3]); + if (indexToPtr == NULL) { + result = TCL_ERROR; + goto done; + } + if (TkTextIndexCmp(indexFromPtr, indexToPtr) > 0) { + Tcl_SetObjResult(interp, Tcl_ObjPrintf( + "index \"%s\" before \"%s\" in the text", + Tcl_GetString(objv[3]), Tcl_GetString(objv[2]))); + Tcl_SetErrorCode(interp, "TK", "TEXT", "INDEX_ORDER", NULL); + result = TCL_ERROR; + goto done; + } + if (textPtr->state == TK_TEXT_STATE_NORMAL) { + int lineNum, byteIndex; + TkTextIndex index; + + /* + * The 'replace' operation is quite complex to do correctly, + * because we want a number of criteria to hold: + * + * 1. The insertion point shouldn't move, unless it is within the + * deleted range. In this case it should end up after the new + * text. + * + * 2. The window should not change the text it shows - should not + * scroll vertically - unless the result of the replace is + * that the insertion position which used to be on-screen is + * now off-screen. + */ + + byteIndex = textPtr->topIndex.byteIndex; + lineNum = TkBTreeLinesTo(textPtr, textPtr->topIndex.linePtr); + + TkTextMarkSegToIndex(textPtr, textPtr->insertMarkPtr, &index); + if ((TkTextIndexCmp(indexFromPtr, &index) < 0) + && (TkTextIndexCmp(indexToPtr, &index) > 0)) { + /* + * The insertion point is inside the range to be replaced, so + * we have to do some calculations to ensure it doesn't move + * unnecessarily. + */ + + int deleteInsertOffset, insertLength, j; + + insertLength = 0; + for (j = 4; j < objc; j += 2) { + insertLength += Tcl_GetCharLength(objv[j]); + } + + /* + * Calculate 'deleteInsertOffset' as an offset we will apply + * to the insertion point after this operation. + */ + + deleteInsertOffset = CountIndices(textPtr, indexFromPtr, + &index, COUNT_CHARS); + if (deleteInsertOffset > insertLength) { + deleteInsertOffset = insertLength; + } + + result = TextReplaceCmd(textPtr, interp, indexFromPtr, + indexToPtr, objc, objv, 0); + + if (result == TCL_OK) { + /* + * Move the insertion position to the correct place. + */ + + TkTextIndexForwChars(NULL, indexFromPtr, + deleteInsertOffset, &index, COUNT_INDICES); + TkBTreeUnlinkSegment(textPtr->insertMarkPtr, + textPtr->insertMarkPtr->body.mark.linePtr); + TkBTreeLinkSegment(textPtr->insertMarkPtr, &index); + } + } else { + result = TextReplaceCmd(textPtr, interp, indexFromPtr, + indexToPtr, objc, objv, 1); + } + if (result == TCL_OK) { + /* + * Now ensure the top-line is in the right place. + */ + + TkTextMakeByteIndex(textPtr->sharedTextPtr->tree, textPtr, + lineNum, byteIndex, &index); + TkTextSetYView(textPtr, &index, TK_TEXT_NOPIXELADJUST); + } + } + break; + } + case TEXT_SCAN: + result = TkTextScanCmd(textPtr, interp, objc, objv); + break; + case TEXT_SEARCH: + result = TextSearchCmd(textPtr, interp, objc, objv); + break; + case TEXT_SEE: + result = TkTextSeeCmd(textPtr, interp, objc, objv); + break; + case TEXT_SYNC: { + if (objc == 4) { + Tcl_Obj *cmd = objv[3]; + const char *option = Tcl_GetString(objv[2]); + if (strncmp(option, "-command", objv[2]->length)) { + Tcl_AppendResult(interp, "wrong option \"", option, "\": should be \"-command\"", NULL); + result = TCL_ERROR; + goto done; + } + Tcl_IncrRefCount(cmd); + if (TkTextPendingsync(textPtr)) { + if (textPtr->afterSyncCmd) { + Tcl_DecrRefCount(textPtr->afterSyncCmd); + } + textPtr->afterSyncCmd = cmd; + } else { + textPtr->afterSyncCmd = cmd; + Tcl_DoWhenIdle(RunAfterSyncCmd, (ClientData) textPtr); + } + break; + } else if (objc != 2) { + Tcl_WrongNumArgs(interp, 2, objv, "?-command command?"); + result = TCL_ERROR; + goto done; + } + if (textPtr->afterSyncCmd) { + Tcl_DecrRefCount(textPtr->afterSyncCmd); + } + textPtr->afterSyncCmd = NULL; + TkTextUpdateLineMetrics(textPtr, 1, + TkBTreeNumLines(textPtr->sharedTextPtr->tree, textPtr), -1); + break; + } + case TEXT_TAG: + result = TkTextTagCmd(textPtr, interp, objc, objv); + break; + case TEXT_WINDOW: + result = TkTextWindowCmd(textPtr, interp, objc, objv); + break; + case TEXT_XVIEW: + result = TkTextXviewCmd(textPtr, interp, objc, objv); + break; + case TEXT_YVIEW: + result = TkTextYviewCmd(textPtr, interp, objc, objv); + break; + } + + done: + textPtr->refCount--; + if (textPtr->refCount == 0) { + ckfree(textPtr); + } + return result; +} + +/* + *-------------------------------------------------------------- + * + * SharedTextObjCmd -- + * + * This function is invoked to process commands on the shared portion of + * a text widget. Currently it is not actually exported as a Tcl command, + * and is only used internally to process parts of undo/redo scripts. + * See the user documentation for 'text' for details on what it does - + * the only subcommands it currently supports are 'insert' and 'delete'. + * + * Results: + * A standard Tcl result. + * + * Side effects: + * See the user documentation for "text". + * + *-------------------------------------------------------------- + */ + +static int +SharedTextObjCmd( + ClientData clientData, /* Information about shared test B-tree. */ + Tcl_Interp *interp, /* Current interpreter. */ + int objc, /* Number of arguments. */ + Tcl_Obj *const objv[]) /* Argument objects. */ +{ + register TkSharedText *sharedPtr = clientData; + int result = TCL_OK; + int index; + + static const char *const optionStrings[] = { + "delete", "insert", NULL + }; + enum options { + TEXT_DELETE, TEXT_INSERT + }; + + if (objc < 2) { + Tcl_WrongNumArgs(interp, 1, objv, "option ?arg ...?"); + return TCL_ERROR; + } + + if (Tcl_GetIndexFromObjStruct(interp, objv[1], optionStrings, + sizeof(char *), "option", 0, &index) != TCL_OK) { + return TCL_ERROR; + } + + switch ((enum options) index) { + case TEXT_DELETE: + if (objc < 3) { + Tcl_WrongNumArgs(interp, 2, objv, "index1 ?index2 ...?"); + return TCL_ERROR; + } + if (objc < 5) { + /* + * Simple case requires no predetermination of indices. + */ + + TkTextIndex index1; + + /* + * Parse the starting and stopping indices. + */ + + result = TkTextSharedGetObjIndex(interp, sharedPtr, objv[2], + &index1); + if (result != TCL_OK) { + return result; + } + if (objc == 4) { + TkTextIndex index2; + + result = TkTextSharedGetObjIndex(interp, sharedPtr, objv[3], + &index2); + if (result != TCL_OK) { + return result; + } + DeleteIndexRange(sharedPtr, NULL, &index1, &index2, 1); + } else { + DeleteIndexRange(sharedPtr, NULL, &index1, NULL, 1); + } + return TCL_OK; + } else { + /* Too many arguments */ + return TCL_ERROR; + } + break; + case TEXT_INSERT: { + TkTextIndex index1; + + if (objc < 4) { + Tcl_WrongNumArgs(interp, 2, objv, + "index chars ?tagList chars tagList ...?"); + return TCL_ERROR; + } + result = TkTextSharedGetObjIndex(interp, sharedPtr, objv[2], + &index1); + if (result != TCL_OK) { + return result; + } + return TextInsertCmd(sharedPtr, NULL, interp, objc-3, objv+3, &index1, + 1); + } + default: + return TCL_OK; + } +} + +/* + *-------------------------------------------------------------- + * + * TextPeerCmd -- + * + * This function is invoked to process the "text peer" Tcl command. See + * the user documentation for details on what it does. + * + * Results: + * A standard Tcl result. + * + * Side effects: + * See the user documentation. + * + *-------------------------------------------------------------- + */ + +static int +TextPeerCmd( + TkText *textPtr, /* Information about text widget. */ + Tcl_Interp *interp, /* Current interpreter. */ + int objc, /* Number of arguments. */ + Tcl_Obj *const objv[]) /* Argument objects. */ +{ + Tk_Window tkwin = textPtr->tkwin; + int index; + + static const char *const peerOptionStrings[] = { + "create", "names", NULL + }; + enum peerOptions { + PEER_CREATE, PEER_NAMES + }; + + if (objc < 3) { + Tcl_WrongNumArgs(interp, 2, objv, "option ?arg ...?"); + return TCL_ERROR; + } + + if (Tcl_GetIndexFromObjStruct(interp, objv[2], peerOptionStrings, + sizeof(char *), "peer option", 0, &index) != TCL_OK) { + return TCL_ERROR; + } + + switch ((enum peerOptions) index) { + case PEER_CREATE: + if (objc < 4) { + Tcl_WrongNumArgs(interp, 3, objv, "pathName ?-option value ...?"); + return TCL_ERROR; + } + return CreateWidget(textPtr->sharedTextPtr, tkwin, interp, textPtr, + objc-2, objv+2); + case PEER_NAMES: { + TkText *tPtr = textPtr->sharedTextPtr->peers; + Tcl_Obj *peersObj; + + if (objc > 3) { + Tcl_WrongNumArgs(interp, 3, objv, NULL); + return TCL_ERROR; + } + peersObj = Tcl_NewObj(); + while (tPtr != NULL) { + if (tPtr != textPtr) { + Tcl_ListObjAppendElement(NULL, peersObj, + TkNewWindowObj(tPtr->tkwin)); + } + tPtr = tPtr->next; + } + Tcl_SetObjResult(interp, peersObj); + } + } + + return TCL_OK; +} + +/* + *---------------------------------------------------------------------- + * + * TextReplaceCmd -- + * + * This function is invoked to process part of the "replace" widget + * command for text widgets. + * + * Results: + * A standard Tcl result. + * + * Side effects: + * See the user documentation. + * + * If 'viewUpdate' is false, then textPtr->topIndex may no longer be a + * valid index after this function returns. The caller is responsible for + * ensuring a correct index is in place. + * + *---------------------------------------------------------------------- + */ + +static int +TextReplaceCmd( + TkText *textPtr, /* Information about text widget. */ + Tcl_Interp *interp, /* Current interpreter. */ + const TkTextIndex *indexFromPtr, + /* Index from which to replace. */ + const TkTextIndex *indexToPtr, + /* Index to which to replace. */ + int objc, /* Number of arguments. */ + Tcl_Obj *const objv[], /* Argument objects. */ + int viewUpdate) /* Update vertical view if set. */ +{ + /* + * Perform the deletion and insertion, but ensure no undo-separator is + * placed between the two operations. Since we are using the helper + * functions 'DeleteIndexRange' and 'TextInsertCmd' we have to pretend + * that the autoSeparators setting is off, so that we don't get an + * undo-separator between the delete and insert. + */ + + int origAutoSep = textPtr->sharedTextPtr->autoSeparators; + int result, lineNumber; + TkTextIndex indexTmp; + + if (textPtr->sharedTextPtr->undo) { + textPtr->sharedTextPtr->autoSeparators = 0; + if (origAutoSep && + textPtr->sharedTextPtr->lastEditMode!=TK_TEXT_EDIT_REPLACE) { + TkUndoInsertUndoSeparator(textPtr->sharedTextPtr->undoStack); + } + } + + /* + * Must save and restore line in indexFromPtr based on line number; can't + * keep the line itself as that might be eliminated/invalidated when + * deleting the range. [Bug 1602537] + */ + + indexTmp = *indexFromPtr; + lineNumber = TkBTreeLinesTo(textPtr, indexFromPtr->linePtr); + DeleteIndexRange(NULL, textPtr, indexFromPtr, indexToPtr, viewUpdate); + indexTmp.linePtr = TkBTreeFindLine(indexTmp.tree, textPtr, lineNumber); + result = TextInsertCmd(NULL, textPtr, interp, objc-4, objv+4, + &indexTmp, viewUpdate); + + if (textPtr->sharedTextPtr->undo) { + textPtr->sharedTextPtr->lastEditMode = TK_TEXT_EDIT_REPLACE; + textPtr->sharedTextPtr->autoSeparators = origAutoSep; + } + + return result; +} + +/* + *---------------------------------------------------------------------- + * + * TextIndexSortProc -- + * + * This function is called by qsort when sorting an array of indices in + * *decreasing* order (last to first). + * + * Results: + * The return value is -1 if the first argument should be before the + * second element, 0 if it's equivalent, and 1 if it should be after the + * second element. + * + * Side effects: + * None. + * + *---------------------------------------------------------------------- + */ + +static int +TextIndexSortProc( + const void *first, /* Elements to be compared. */ + const void *second) +{ + TkTextIndex *pair1 = (TkTextIndex *) first; + TkTextIndex *pair2 = (TkTextIndex *) second; + int cmp = TkTextIndexCmp(&pair1[1], &pair2[1]); + + if (cmp == 0) { + /* + * If the first indices were equal, we want the second index of the + * pair also to be the greater. Use pointer magic to access the second + * index pair. + */ + + cmp = TkTextIndexCmp(&pair1[0], &pair2[0]); + } + if (cmp > 0) { + return -1; + } else if (cmp < 0) { + return 1; + } + return 0; +} + +/* + *---------------------------------------------------------------------- + * + * DestroyText -- + * + * This function is invoked when we receive a destroy event to clean up + * the internal structure of a text widget. We will free up most of the + * internal structure and delete the associated Tcl command. If there are + * no outstanding references to the widget, we also free up the textPtr + * itself. + * + * The widget has already been flagged as deleted. + * + * Results: + * None. + * + * Side effects: + * Either everything or almost everything associated with the text is + * freed up. + * + *---------------------------------------------------------------------- + */ + +static void +DestroyText( + TkText *textPtr) /* Info about text widget. */ +{ + Tcl_HashSearch search; + Tcl_HashEntry *hPtr; + TkTextTag *tagPtr; + TkSharedText *sharedTextPtr = textPtr->sharedTextPtr; + + /* + * Free up all the stuff that requires special handling. We have already + * called let Tk_FreeConfigOptions to handle all the standard + * option-related stuff (and so none of that exists when we are called). + * Special note: free up display-related information before deleting the + * B-tree, since display-related stuff may refer to stuff in the B-tree. + */ + + TkTextFreeDInfo(textPtr); + textPtr->dInfoPtr = NULL; + + /* + * Remove ourselves from the peer list. + */ + + if (sharedTextPtr->peers == textPtr) { + sharedTextPtr->peers = textPtr->next; + } else { + TkText *nextPtr = sharedTextPtr->peers; + while (nextPtr != NULL) { + if (nextPtr->next == textPtr) { + nextPtr->next = textPtr->next; + break; + } + nextPtr = nextPtr->next; + } + } + + /* + * Always clean up the widget-specific tags first. Common tags (i.e. most) + * will only be cleaned up when the shared structure is cleaned up. + * + * We also need to clean up widget-specific marks ('insert', 'current'), + * since otherwise marks will never disappear from the B-tree. + */ + + TkTextDeleteTag(textPtr, textPtr->selTagPtr); + TkBTreeUnlinkSegment(textPtr->insertMarkPtr, + textPtr->insertMarkPtr->body.mark.linePtr); + ckfree(textPtr->insertMarkPtr); + TkBTreeUnlinkSegment(textPtr->currentMarkPtr, + textPtr->currentMarkPtr->body.mark.linePtr); + ckfree(textPtr->currentMarkPtr); + + /* + * Now we've cleaned up everything of relevance to us in the B-tree, so we + * disassociate outselves from it. + * + * When the refCount reaches zero, it's time to clean up the shared + * portion of the text widget. + */ + + sharedTextPtr->refCount--; + + if (sharedTextPtr->refCount > 0) { + TkBTreeRemoveClient(sharedTextPtr->tree, textPtr); + + /* + * Free up any embedded windows which belong to this widget. + */ + + for (hPtr = Tcl_FirstHashEntry(&sharedTextPtr->windowTable, &search); + hPtr != NULL; hPtr = Tcl_NextHashEntry(&search)) { + TkTextEmbWindowClient *loop; + TkTextSegment *ewPtr = Tcl_GetHashValue(hPtr); + + loop = ewPtr->body.ew.clients; + if (loop->textPtr == textPtr) { + ewPtr->body.ew.clients = loop->next; + TkTextWinFreeClient(hPtr, loop); + } else { + TkTextEmbWindowClient *client = ewPtr->body.ew.clients; + + client = loop->next; + while (client != NULL) { + if (client->textPtr == textPtr) { + loop->next = client->next; + TkTextWinFreeClient(hPtr, client); + break; + } else { + loop = loop->next; + } + client = loop->next; + } + } + } + } else { + /* + * No need to call 'TkBTreeRemoveClient' first, since this will do + * everything in one go, more quickly. + */ + + TkBTreeDestroy(sharedTextPtr->tree); + + for (hPtr = Tcl_FirstHashEntry(&sharedTextPtr->tagTable, &search); + hPtr != NULL; hPtr = Tcl_NextHashEntry(&search)) { + tagPtr = Tcl_GetHashValue(hPtr); + + /* + * No need to use 'TkTextDeleteTag' since we've already removed + * the B-tree completely. + */ + + TkTextFreeTag(textPtr, tagPtr); + } + Tcl_DeleteHashTable(&sharedTextPtr->tagTable); + for (hPtr = Tcl_FirstHashEntry(&sharedTextPtr->markTable, &search); + hPtr != NULL; hPtr = Tcl_NextHashEntry(&search)) { + ckfree(Tcl_GetHashValue(hPtr)); + } + Tcl_DeleteHashTable(&sharedTextPtr->markTable); + TkUndoFreeStack(sharedTextPtr->undoStack); + + Tcl_DeleteHashTable(&sharedTextPtr->windowTable); + Tcl_DeleteHashTable(&sharedTextPtr->imageTable); + + if (sharedTextPtr->bindingTable != NULL) { + Tk_DeleteBindingTable(sharedTextPtr->bindingTable); + } + ckfree(sharedTextPtr); + } + + if (textPtr->tabArrayPtr != NULL) { + ckfree(textPtr->tabArrayPtr); + } + if (textPtr->insertBlinkHandler != NULL) { + Tcl_DeleteTimerHandler(textPtr->insertBlinkHandler); + } + + textPtr->tkwin = NULL; + textPtr->refCount--; + Tcl_DeleteCommandFromToken(textPtr->interp, textPtr->widgetCmd); + if (textPtr->afterSyncCmd){ + Tcl_DecrRefCount(textPtr->afterSyncCmd); + textPtr->afterSyncCmd = NULL; + } + if (textPtr->refCount == 0) { + ckfree(textPtr); + } +} + +/* + *---------------------------------------------------------------------- + * + * ConfigureText -- + * + * This function is called to process an objv/objc list, plus the Tk + * option database, in order to configure (or reconfigure) a text widget. + * + * Results: + * The return value is a standard Tcl result. If TCL_ERROR is returned, + * then the interp's result contains an error message. + * + * Side effects: + * Configuration information, such as text string, colors, font, etc. get + * set for textPtr; old resources get freed, if there were any. + * + *---------------------------------------------------------------------- + */ + +static int +ConfigureText( + Tcl_Interp *interp, /* Used for error reporting. */ + register TkText *textPtr, /* Information about widget; may or may not + * already have values for some fields. */ + int objc, /* Number of arguments. */ + Tcl_Obj *const objv[]) /* Argument objects. */ +{ + Tk_SavedOptions savedOptions; + int oldExport = textPtr->exportSelection; + int mask = 0; + + if (Tk_SetOptions(interp, (char *) textPtr, textPtr->optionTable, + objc, objv, textPtr->tkwin, &savedOptions, &mask) != TCL_OK) { + return TCL_ERROR; + } + + /* + * Copy down shared flags. + */ + + textPtr->sharedTextPtr->undo = textPtr->undo; + textPtr->sharedTextPtr->maxUndo = textPtr->maxUndo; + textPtr->sharedTextPtr->autoSeparators = textPtr->autoSeparators; + + TkUndoSetMaxDepth(textPtr->sharedTextPtr->undoStack, + textPtr->sharedTextPtr->maxUndo); + + /* + * A few other options also need special processing, such as parsing the + * geometry and setting the background from a 3-D border. + */ + + Tk_SetBackgroundFromBorder(textPtr->tkwin, textPtr->border); + + if (mask & TK_TEXT_LINE_RANGE) { + int start, end, current; + TkTextIndex index1, index2, index3; + + /* + * Line start and/or end have been adjusted. We need to validate the + * first displayed line and arrange for re-layout. + */ + + TkBTreeClientRangeChanged(textPtr, textPtr->charHeight); + + if (textPtr->start != NULL) { + start = TkBTreeLinesTo(NULL, textPtr->start); + } else { + start = 0; + } + if (textPtr->end != NULL) { + end = TkBTreeLinesTo(NULL, textPtr->end); + } else { + end = TkBTreeNumLines(textPtr->sharedTextPtr->tree, NULL); + } + if (start > end) { + Tcl_SetObjResult(interp, Tcl_NewStringObj( + "-startline must be less than or equal to -endline", -1)); + Tcl_SetErrorCode(interp, "TK", "TEXT", "INDEX_ORDER", NULL); + Tk_RestoreSavedOptions(&savedOptions); + return TCL_ERROR; + } + current = TkBTreeLinesTo(NULL, textPtr->topIndex.linePtr); + TkTextMakeByteIndex(textPtr->sharedTextPtr->tree, NULL, start, 0, + &index1); + TkTextMakeByteIndex(textPtr->sharedTextPtr->tree, NULL, end, 0, + &index2); + if (current < start || current > end) { + TkTextSearch search; + TkTextIndex first, last; + int selChanged = 0; + + TkTextSetYView(textPtr, &index1, 0); + + /* + * We may need to adjust the selection. So we have to check + * whether the "sel" tag was applied to anything outside the + * current start,end. + */ + + TkTextMakeByteIndex(textPtr->sharedTextPtr->tree, NULL, 0, 0, + &first); + TkTextMakeByteIndex(textPtr->sharedTextPtr->tree, NULL, + TkBTreeNumLines(textPtr->sharedTextPtr->tree, NULL), + 0, &last); + TkBTreeStartSearch(&first, &last, textPtr->selTagPtr, &search); + if (!TkBTreeCharTagged(&first, textPtr->selTagPtr) + && !TkBTreeNextTag(&search)) { + /* Nothing tagged with "sel" */ + } else { + int line = TkBTreeLinesTo(NULL, search.curIndex.linePtr); + + if (line < start) { + selChanged = 1; + } else { + TkTextLine *linePtr = search.curIndex.linePtr; + + while (TkBTreeNextTag(&search)) { + linePtr = search.curIndex.linePtr; + } + line = TkBTreeLinesTo(NULL, linePtr); + if (line >= end) { + selChanged = 1; + } + } + } + if (selChanged) { + /* + * Send an event that the selection has changed, and abort any + * partial-selections in progress. + */ + + TkTextSelectionEvent(textPtr); + textPtr->abortSelections = 1; + } + } + + /* Indices are potentially obsolete after changing -startline and/or + * -endline, therefore increase the epoch. + * Also, clamp the insert and current (unshared) marks to the new + * -startline/-endline range limits of the widget. All other (shared) + * marks are unchanged. + * The return value of TkTextMarkNameToIndex does not need to be + * checked: "insert" and "current" marks always exist, and the + * purpose of the code below precisely is to move them inside the + * -startline/-endline range. + */ + + textPtr->sharedTextPtr->stateEpoch++; + TkTextMarkNameToIndex(textPtr, "insert", &index3); + if (TkTextIndexCmp(&index3, &index1) < 0) { + textPtr->insertMarkPtr = TkTextSetMark(textPtr, "insert", &index1); + } + if (TkTextIndexCmp(&index3, &index2) > 0) { + textPtr->insertMarkPtr = TkTextSetMark(textPtr, "insert", &index2); + } + TkTextMarkNameToIndex(textPtr, "current", &index3); + if (TkTextIndexCmp(&index3, &index1) < 0) { + textPtr->currentMarkPtr = TkTextSetMark(textPtr, "current", &index1); + } + if (TkTextIndexCmp(&index3, &index2) > 0) { + textPtr->currentMarkPtr = TkTextSetMark(textPtr, "current", &index2); + } + } + + /* + * Don't allow negative spacings. + */ + + if (textPtr->spacing1 < 0) { + textPtr->spacing1 = 0; + } + if (textPtr->spacing2 < 0) { + textPtr->spacing2 = 0; + } + if (textPtr->spacing3 < 0) { + textPtr->spacing3 = 0; + } + + /* + * Parse tab stops. + */ + + if (textPtr->tabArrayPtr != NULL) { + ckfree(textPtr->tabArrayPtr); + textPtr->tabArrayPtr = NULL; + } + if (textPtr->tabOptionPtr != NULL) { + textPtr->tabArrayPtr = TkTextGetTabs(interp, textPtr, + textPtr->tabOptionPtr); + if (textPtr->tabArrayPtr == NULL) { + Tcl_AddErrorInfo(interp,"\n (while processing -tabs option)"); + Tk_RestoreSavedOptions(&savedOptions); + return TCL_ERROR; + } + } + + /* + * Make sure that configuration options are properly mirrored between the + * widget record and the "sel" tags. NOTE: we don't have to free up + * information during the mirroring; old information was freed when it was + * replaced in the widget record. + */ + + if (textPtr->selTagPtr->selBorder == NULL) { + textPtr->selTagPtr->border = textPtr->selBorder; + } else { + textPtr->selTagPtr->selBorder = textPtr->selBorder; + } + if (textPtr->selTagPtr->borderWidthPtr != textPtr->selBorderWidthPtr) { + textPtr->selTagPtr->borderWidthPtr = textPtr->selBorderWidthPtr; + textPtr->selTagPtr->borderWidth = textPtr->selBorderWidth; + } + if (textPtr->selTagPtr->selFgColor == NULL) { + textPtr->selTagPtr->fgColor = textPtr->selFgColorPtr; + } else { + textPtr->selTagPtr->selFgColor = textPtr->selFgColorPtr; + } + textPtr->selTagPtr->affectsDisplay = 0; + textPtr->selTagPtr->affectsDisplayGeometry = 0; + if ((textPtr->selTagPtr->elideString != NULL) + || (textPtr->selTagPtr->tkfont != None) + || (textPtr->selTagPtr->justifyString != NULL) + || (textPtr->selTagPtr->lMargin1String != NULL) + || (textPtr->selTagPtr->lMargin2String != NULL) + || (textPtr->selTagPtr->offsetString != NULL) + || (textPtr->selTagPtr->rMarginString != NULL) + || (textPtr->selTagPtr->spacing1String != NULL) + || (textPtr->selTagPtr->spacing2String != NULL) + || (textPtr->selTagPtr->spacing3String != NULL) + || (textPtr->selTagPtr->tabStringPtr != NULL) + || (textPtr->selTagPtr->wrapMode != TEXT_WRAPMODE_NULL)) { + textPtr->selTagPtr->affectsDisplay = 1; + textPtr->selTagPtr->affectsDisplayGeometry = 1; + } + if ((textPtr->selTagPtr->border != NULL) + || (textPtr->selTagPtr->selBorder != NULL) + || (textPtr->selTagPtr->reliefString != NULL) + || (textPtr->selTagPtr->bgStipple != None) + || (textPtr->selTagPtr->fgColor != NULL) + || (textPtr->selTagPtr->selFgColor != NULL) + || (textPtr->selTagPtr->fgStipple != None) + || (textPtr->selTagPtr->overstrikeString != NULL) + || (textPtr->selTagPtr->overstrikeColor != NULL) + || (textPtr->selTagPtr->underlineString != NULL) + || (textPtr->selTagPtr->underlineColor != NULL) + || (textPtr->selTagPtr->lMarginColor != NULL) + || (textPtr->selTagPtr->rMarginColor != NULL)) { + textPtr->selTagPtr->affectsDisplay = 1; + } + TkTextRedrawTag(NULL, textPtr, NULL, NULL, textPtr->selTagPtr, 1); + + /* + * Claim the selection if we've suddenly started exporting it and there + * are tagged characters. + */ + + if (textPtr->exportSelection && (!oldExport)) { + TkTextSearch search; + TkTextIndex first, last; + + TkTextMakeByteIndex(textPtr->sharedTextPtr->tree, textPtr, 0, 0, + &first); + TkTextMakeByteIndex(textPtr->sharedTextPtr->tree, textPtr, + TkBTreeNumLines(textPtr->sharedTextPtr->tree, textPtr), + 0, &last); + TkBTreeStartSearch(&first, &last, textPtr->selTagPtr, &search); + if (TkBTreeCharTagged(&first, textPtr->selTagPtr) + || TkBTreeNextTag(&search)) { + Tk_OwnSelection(textPtr->tkwin, XA_PRIMARY, TkTextLostSelection, + textPtr); + textPtr->flags |= GOT_SELECTION; + } + } + + /* + * Account for state changes that would reenable blinking cursor state. + */ + + if (textPtr->flags & GOT_FOCUS) { + Tcl_DeleteTimerHandler(textPtr->insertBlinkHandler); + textPtr->insertBlinkHandler = NULL; + TextBlinkProc(textPtr); + } + + /* + * Register the desired geometry for the window, and arrange for the + * window to be redisplayed. + */ + + if (textPtr->width <= 0) { + textPtr->width = 1; + } + if (textPtr->height <= 0) { + textPtr->height = 1; + } + Tk_FreeSavedOptions(&savedOptions); + TextWorldChanged(textPtr, mask); + return TCL_OK; +} + +/* + *--------------------------------------------------------------------------- + * + * TextWorldChangedCallback -- + * + * This function is called when the world has changed in some way and the + * widget needs to recompute all its graphics contexts and determine its + * new geometry. + * + * Results: + * None. + * + * Side effects: + * Configures all tags in the Text with a empty objc/objv, for the side + * effect of causing all the items to recompute their geometry and to be + * redisplayed. + * + *--------------------------------------------------------------------------- + */ + +static void +TextWorldChangedCallback( + ClientData instanceData) /* Information about widget. */ +{ + TkText *textPtr = instanceData; + + TextWorldChanged(textPtr, TK_TEXT_LINE_GEOMETRY); +} + +/* + *--------------------------------------------------------------------------- + * + * TextWorldChanged -- + * + * This function is called when the world has changed in some way and the + * widget needs to recompute all its graphics contexts and determine its + * new geometry. + * + * Results: + * None. + * + * Side effects: + * Configures all tags in the Text with a empty objc/objv, for the side + * effect of causing all the items to recompute their geometry and to be + * redisplayed. + * + *--------------------------------------------------------------------------- + */ + +static void +TextWorldChanged( + TkText *textPtr, /* Information about widget. */ + int mask) /* OR'd collection of bits showing what has + * changed. */ +{ + Tk_FontMetrics fm; + int border; + int oldCharHeight = textPtr->charHeight; + + textPtr->charWidth = Tk_TextWidth(textPtr->tkfont, "0", 1); + if (textPtr->charWidth <= 0) { + textPtr->charWidth = 1; + } + Tk_GetFontMetrics(textPtr->tkfont, &fm); + + textPtr->charHeight = fm.linespace; + if (textPtr->charHeight <= 0) { + textPtr->charHeight = 1; + } + if (textPtr->charHeight != oldCharHeight) { + TkBTreeClientRangeChanged(textPtr, textPtr->charHeight); + } + border = textPtr->borderWidth + textPtr->highlightWidth; + Tk_GeometryRequest(textPtr->tkwin, + textPtr->width * textPtr->charWidth + 2*textPtr->padX + 2*border, + textPtr->height*(fm.linespace+textPtr->spacing1+textPtr->spacing3) + + 2*textPtr->padY + 2*border); + + Tk_SetInternalBorderEx(textPtr->tkwin, + border + textPtr->padX, border + textPtr->padX, + border + textPtr->padY, border + textPtr->padY); + if (textPtr->setGrid) { + Tk_SetGrid(textPtr->tkwin, textPtr->width, textPtr->height, + textPtr->charWidth, textPtr->charHeight); + } else { + Tk_UnsetGrid(textPtr->tkwin); + } + + TkTextRelayoutWindow(textPtr, mask); +} + +/* + *-------------------------------------------------------------- + * + * TextEventProc -- + * + * This function is invoked by the Tk dispatcher on structure changes to + * a text. For texts with 3D borders, this function is also invoked for + * exposures. + * + * Results: + * None. + * + * Side effects: + * When the window gets deleted, internal structures get cleaned up. + * When it gets exposed, it is redisplayed. + * + *-------------------------------------------------------------- + */ + +static void +TextEventProc( + ClientData clientData, /* Information about window. */ + register XEvent *eventPtr) /* Information about event. */ +{ + register TkText *textPtr = clientData; + TkTextIndex index, index2; + + if (eventPtr->type == Expose) { + TkTextRedrawRegion(textPtr, eventPtr->xexpose.x, + eventPtr->xexpose.y, eventPtr->xexpose.width, + eventPtr->xexpose.height); + } else if (eventPtr->type == ConfigureNotify) { + if ((textPtr->prevWidth != Tk_Width(textPtr->tkwin)) + || (textPtr->prevHeight != Tk_Height(textPtr->tkwin))) { + int mask = 0; + + if (textPtr->prevWidth != Tk_Width(textPtr->tkwin)) { + mask = TK_TEXT_LINE_GEOMETRY; + } + TkTextRelayoutWindow(textPtr, mask); + textPtr->prevWidth = Tk_Width(textPtr->tkwin); + textPtr->prevHeight = Tk_Height(textPtr->tkwin); + } + } else if (eventPtr->type == DestroyNotify) { + /* + * NOTE: we must zero out selBorder, selBorderWidthPtr and + * selFgColorPtr: they are duplicates of information in the "sel" tag, + * which will be freed up when we delete all tags. Hence we don't want + * the automatic config options freeing process to delete them as + * well. + */ + + textPtr->selBorder = NULL; + textPtr->selBorderWidthPtr = NULL; + textPtr->selBorderWidth = 0; + textPtr->selFgColorPtr = NULL; + if (textPtr->setGrid) { + Tk_UnsetGrid(textPtr->tkwin); + textPtr->setGrid = 0; + } + if (!(textPtr->flags & OPTIONS_FREED)) { + Tk_FreeConfigOptions((char *) textPtr, textPtr->optionTable, + textPtr->tkwin); + textPtr->flags |= OPTIONS_FREED; + } + textPtr->flags |= DESTROYED; + + /* + * Call 'DestroyTest' to handle the deletion for us. The actual + * textPtr may still exist after this, if there are some outstanding + * references. But we have flagged it as DESTROYED just above, so + * nothing will try to make use of it very extensively. + */ + + DestroyText(textPtr); + } else if ((eventPtr->type == FocusIn) || (eventPtr->type == FocusOut)) { + if (eventPtr->xfocus.detail == NotifyInferior + || eventPtr->xfocus.detail == NotifyAncestor + || eventPtr->xfocus.detail == NotifyNonlinear) { + Tcl_DeleteTimerHandler(textPtr->insertBlinkHandler); + if (eventPtr->type == FocusIn) { + textPtr->flags |= GOT_FOCUS | INSERT_ON; + if (textPtr->insertOffTime != 0) { + textPtr->insertBlinkHandler = Tcl_CreateTimerHandler( + textPtr->insertOnTime, TextBlinkProc, textPtr); + } + } else { + textPtr->flags &= ~(GOT_FOCUS | INSERT_ON); + textPtr->insertBlinkHandler = NULL; + } + if (textPtr->inactiveSelBorder != textPtr->selBorder) { + TkTextRedrawTag(NULL, textPtr, NULL, NULL, textPtr->selTagPtr, + 1); + } + TkTextMarkSegToIndex(textPtr, textPtr->insertMarkPtr, &index); + TkTextIndexForwChars(NULL, &index, 1, &index2, COUNT_INDICES); + + /* + * While we wish to redisplay, no heights have changed, so no need + * to call TkTextInvalidateLineMetrics. + */ + + TkTextChanged(NULL, textPtr, &index, &index2); + if (textPtr->highlightWidth > 0) { + TkTextRedrawRegion(textPtr, 0, 0, textPtr->highlightWidth, + textPtr->highlightWidth); + } + } + } +} + +/* + *---------------------------------------------------------------------- + * + * TextCmdDeletedProc -- + * + * This function is invoked when a widget command is deleted. If the + * widget isn't already in the process of being destroyed, this command + * destroys it. + * + * Results: + * None. + * + * Side effects: + * The widget is destroyed. + * + *---------------------------------------------------------------------- + */ + +static void +TextCmdDeletedProc( + ClientData clientData) /* Pointer to widget record for widget. */ +{ + TkText *textPtr = clientData; + Tk_Window tkwin = textPtr->tkwin; + + /* + * This function could be invoked either because the window was destroyed + * and the command was then deleted (in which this flag is already set) or + * because the command was deleted, and then this function destroys the + * widget. + */ + + if (!(textPtr->flags & DESTROYED)) { + if (textPtr->setGrid) { + Tk_UnsetGrid(textPtr->tkwin); + textPtr->setGrid = 0; + } + textPtr->flags |= DESTROYED; + Tk_DestroyWindow(tkwin); + } +} + +/* + *---------------------------------------------------------------------- + * + * InsertChars -- + * + * This function implements most of the functionality of the "insert" + * widget command. + * + * Results: + * The length of the inserted string. + * + * Side effects: + * The characters in "stringPtr" get added to the text just before the + * character indicated by "indexPtr". + * + * If 'viewUpdate' is true, we may adjust the window contents' + * y-position, and scrollbar setting. + * + *---------------------------------------------------------------------- + */ + +static int +InsertChars( + TkSharedText *sharedTextPtr, + TkText *textPtr, /* Overall information about text widget. */ + TkTextIndex *indexPtr, /* Where to insert new characters. May be + * modified if the index is not valid for + * insertion (e.g. if at "end"). */ + Tcl_Obj *stringPtr, /* Null-terminated string containing new + * information to add to text. */ + int viewUpdate) /* Update the view if set. */ +{ + int lineIndex, length; + TkText *tPtr; + int *lineAndByteIndex; + int resetViewCount; + int pixels[2*PIXEL_CLIENTS]; + const char *string = Tcl_GetString(stringPtr); + + length = stringPtr->length; + if (sharedTextPtr == NULL) { + sharedTextPtr = textPtr->sharedTextPtr; + } + + /* + * Don't allow insertions on the last (dummy) line of the text. This is + * the only place in this function where the indexPtr is modified. + */ + + lineIndex = TkBTreeLinesTo(textPtr, indexPtr->linePtr); + if (lineIndex == TkBTreeNumLines(sharedTextPtr->tree, textPtr)) { + lineIndex--; + TkTextMakeByteIndex(sharedTextPtr->tree, textPtr, lineIndex, 1000000, + indexPtr); + } + + /* + * Notify the display module that lines are about to change, then do the + * insertion. If the insertion occurs on the top line of the widget + * (textPtr->topIndex), then we have to recompute topIndex after the + * insertion, since the insertion could invalidate it. + */ + + resetViewCount = 0; + if (sharedTextPtr->refCount > PIXEL_CLIENTS) { + lineAndByteIndex = ckalloc(sizeof(int) * 2 * sharedTextPtr->refCount); + } else { + lineAndByteIndex = pixels; + } + for (tPtr = sharedTextPtr->peers; tPtr != NULL ; tPtr = tPtr->next) { + lineAndByteIndex[resetViewCount] = -1; + if (indexPtr->linePtr == tPtr->topIndex.linePtr) { + lineAndByteIndex[resetViewCount] = + TkBTreeLinesTo(tPtr, indexPtr->linePtr); + lineAndByteIndex[resetViewCount+1] = tPtr->topIndex.byteIndex; + if (lineAndByteIndex[resetViewCount+1] > indexPtr->byteIndex) { + lineAndByteIndex[resetViewCount+1] += length; + } + } + resetViewCount += 2; + } + + TkTextChanged(sharedTextPtr, NULL, indexPtr, indexPtr); + + sharedTextPtr->stateEpoch++; + + TkBTreeInsertChars(sharedTextPtr->tree, indexPtr, string); + + /* + * Push the insertion on the undo stack, and update the modified status of + * the widget. + */ + + if (length > 0) { + if (sharedTextPtr->undo) { + TkTextIndex toIndex; + + if (sharedTextPtr->autoSeparators && + sharedTextPtr->lastEditMode != TK_TEXT_EDIT_INSERT) { + TkUndoInsertUndoSeparator(sharedTextPtr->undoStack); + } + + sharedTextPtr->lastEditMode = TK_TEXT_EDIT_INSERT; + + TkTextIndexForwBytes(textPtr, indexPtr, length, &toIndex); + TextPushUndoAction(textPtr, stringPtr, 1, indexPtr, &toIndex); + } + + UpdateDirtyFlag(sharedTextPtr); + } + + resetViewCount = 0; + for (tPtr = sharedTextPtr->peers; tPtr != NULL ; tPtr = tPtr->next) { + if (lineAndByteIndex[resetViewCount] != -1) { + if ((tPtr != textPtr) || viewUpdate) { + TkTextIndex newTop; + + TkTextMakeByteIndex(sharedTextPtr->tree, tPtr, + lineAndByteIndex[resetViewCount], 0, &newTop); + TkTextIndexForwBytes(tPtr, &newTop, + lineAndByteIndex[resetViewCount+1], &newTop); + TkTextSetYView(tPtr, &newTop, 0); + } + } + resetViewCount += 2; + } + if (sharedTextPtr->refCount > PIXEL_CLIENTS) { + ckfree(lineAndByteIndex); + } + + /* + * Invalidate any selection retrievals in progress. + */ + + for (tPtr = sharedTextPtr->peers; tPtr != NULL ; tPtr = tPtr->next) { + tPtr->abortSelections = 1; + } + + /* + * For convenience, return the length of the string. + */ + + return length; +} + +/* + *---------------------------------------------------------------------- + * + * TextPushUndoAction -- + * + * Shared by insert and delete actions. Stores the appropriate scripts + * into our undo stack. We will add a single refCount to the 'undoString' + * object, so, if it previously had a refCount of zero, the caller should + * not free it. + * + * Results: + * None. + * + * Side effects: + * Items pushed onto stack. + * + *---------------------------------------------------------------------- + */ + +static void +TextPushUndoAction( + TkText *textPtr, /* Overall information about text widget. */ + Tcl_Obj *undoString, /* New text. */ + int insert, /* 1 if insert, else delete. */ + const TkTextIndex *index1Ptr, + /* Index describing first location. */ + const TkTextIndex *index2Ptr) + /* Index describing second location. */ +{ + TkUndoSubAtom *iAtom, *dAtom; + int canUndo, canRedo; + + /* + * Create the helpers. + */ + + Tcl_Obj *seeInsertObj = Tcl_NewObj(); + Tcl_Obj *markSet1InsertObj = Tcl_NewObj(); + Tcl_Obj *markSet2InsertObj = NULL; + Tcl_Obj *insertCmdObj = Tcl_NewObj(); + Tcl_Obj *deleteCmdObj = Tcl_NewObj(); + + /* + * Get the index positions. + */ + + Tcl_Obj *index1Obj = TkTextNewIndexObj(NULL, index1Ptr); + Tcl_Obj *index2Obj = TkTextNewIndexObj(NULL, index2Ptr); + + /* + * These need refCounts, because they are used more than once below. + */ + + Tcl_IncrRefCount(seeInsertObj); + Tcl_IncrRefCount(index1Obj); + Tcl_IncrRefCount(index2Obj); + + Tcl_ListObjAppendElement(NULL, seeInsertObj, + Tcl_NewStringObj(Tk_PathName(textPtr->tkwin), -1)); + Tcl_ListObjAppendElement(NULL, seeInsertObj, Tcl_NewStringObj("see", 3)); + Tcl_ListObjAppendElement(NULL, seeInsertObj, + Tcl_NewStringObj("insert", 6)); + + Tcl_ListObjAppendElement(NULL, markSet1InsertObj, + Tcl_NewStringObj(Tk_PathName(textPtr->tkwin), -1)); + Tcl_ListObjAppendElement(NULL, markSet1InsertObj, + Tcl_NewStringObj("mark", 4)); + Tcl_ListObjAppendElement(NULL, markSet1InsertObj, + Tcl_NewStringObj("set", 3)); + Tcl_ListObjAppendElement(NULL, markSet1InsertObj, + Tcl_NewStringObj("insert", 6)); + markSet2InsertObj = Tcl_DuplicateObj(markSet1InsertObj); + Tcl_ListObjAppendElement(NULL, markSet1InsertObj, index1Obj); + Tcl_ListObjAppendElement(NULL, markSet2InsertObj, index2Obj); + + Tcl_ListObjAppendElement(NULL, insertCmdObj, + Tcl_NewStringObj("insert", 6)); + Tcl_ListObjAppendElement(NULL, insertCmdObj, index1Obj); + + /* + * Only use of 'undoString' is here. + */ + + Tcl_ListObjAppendElement(NULL, insertCmdObj, undoString); + + Tcl_ListObjAppendElement(NULL, deleteCmdObj, + Tcl_NewStringObj("delete", 6)); + Tcl_ListObjAppendElement(NULL, deleteCmdObj, index1Obj); + Tcl_ListObjAppendElement(NULL, deleteCmdObj, index2Obj); + + /* + * Note: we don't wish to use textPtr->widgetCmd in these callbacks + * because if we delete the textPtr, but peers still exist, we will then + * have references to a non-existent Tcl_Command in the undo stack, which + * will lead to crashes later. Also, the behaviour of the widget w.r.t. + * bindings (%W substitutions) always uses the widget path name, so there + * is no good reason the undo stack should do otherwise. + * + * For the 'insert' and 'delete' actions, we have to register a functional + * callback, because these actions are defined to operate on the + * underlying data shared by all peers. + */ + + iAtom = TkUndoMakeSubAtom(&TextUndoRedoCallback, textPtr->sharedTextPtr, + insertCmdObj, NULL); + TkUndoMakeCmdSubAtom(NULL, markSet2InsertObj, iAtom); + TkUndoMakeCmdSubAtom(NULL, seeInsertObj, iAtom); + + dAtom = TkUndoMakeSubAtom(&TextUndoRedoCallback, textPtr->sharedTextPtr, + deleteCmdObj, NULL); + TkUndoMakeCmdSubAtom(NULL, markSet1InsertObj, dAtom); + TkUndoMakeCmdSubAtom(NULL, seeInsertObj, dAtom); + + Tcl_DecrRefCount(seeInsertObj); + Tcl_DecrRefCount(index1Obj); + Tcl_DecrRefCount(index2Obj); + + canUndo = TkUndoCanUndo(textPtr->sharedTextPtr->undoStack); + canRedo = TkUndoCanRedo(textPtr->sharedTextPtr->undoStack); + + /* + * Depending whether the action is to insert or delete, we provide the + * appropriate second and third arguments to TkUndoPushAction. (The first + * is the 'actionCommand', and the second the 'revertCommand'). + */ + + if (insert) { + TkUndoPushAction(textPtr->sharedTextPtr->undoStack, iAtom, dAtom); + } else { + TkUndoPushAction(textPtr->sharedTextPtr->undoStack, dAtom, iAtom); + } + + if (!canUndo || canRedo) { + GenerateUndoStackEvent(textPtr); + } +} + +/* + *---------------------------------------------------------------------- + * + * TextUndoRedoCallback -- + * + * This function is registered with the generic undo/redo code to handle + * 'insert' and 'delete' actions on all text widgets. We cannot perform + * those actions on any particular text widget, because that text widget + * might have been deleted by the time we get here. + * + * Results: + * A standard Tcl result. + * + * Side effects: + * Will insert or delete text, depending on the first word contained in + * objPtr. + * + *---------------------------------------------------------------------- + */ + +int +TextUndoRedoCallback( + Tcl_Interp *interp, /* Current interpreter. */ + ClientData clientData, /* Passed from undo code, but contains our + * shared text data structure. */ + Tcl_Obj *objPtr) /* Arguments of a command to be handled by the + * shared text data structure. */ +{ + TkSharedText *sharedPtr = clientData; + int res, objc; + Tcl_Obj **objv; + TkText *textPtr; + + res = Tcl_ListObjGetElements(interp, objPtr, &objc, &objv); + if (res != TCL_OK) { + return res; + } + + /* + * If possible, use a real text widget to perform the undo/redo action + * (i.e. insertion or deletion of text). This provides maximum + * compatibility with older versions of Tk, in which the user may rename + * the text widget to allow capture of undo or redo actions. + * + * In particular, this sorting of capture is useful in text editors based + * on the Tk text widget, which need to know which new text needs + * re-coloring. + * + * It would be better if the text widget provided some other mechanism to + * allow capture of this information ("What has just changed in the text + * widget?"). What we have here is not entirely satisfactory under all + * circumstances. + */ + + textPtr = sharedPtr->peers; + while (textPtr != NULL) { + if (textPtr->start == NULL && textPtr->end == NULL) { + Tcl_Obj *cmdNameObj, *evalObj; + + evalObj = Tcl_NewObj(); + Tcl_IncrRefCount(evalObj); + + /* + * We might wish to use the real, current command-name for the + * widget, but this will break any code that has over-ridden the + * widget, and is expecting to observe the insert/delete actions + * which are caused by undo/redo operations. + * + * cmdNameObj = Tcl_NewObj(); + * Tcl_GetCommandFullName(interp, textPtr->widgetCmd, cmdNameObj); + * + * While such interception is not explicitly documented as + * supported, it does occur, and so until we can provide some + * alternative mechanism for such code to do what it needs, we + * allow it to take place here. + */ + + cmdNameObj = Tcl_NewStringObj(Tk_PathName(textPtr->tkwin), -1); + Tcl_ListObjAppendElement(NULL, evalObj, cmdNameObj); + Tcl_ListObjAppendList(NULL, evalObj, objPtr); + res = Tcl_EvalObjEx(interp, evalObj, TCL_EVAL_GLOBAL); + Tcl_DecrRefCount(evalObj); + return res; + } + textPtr = textPtr->next; + } + + /* + * If there's no current text widget which shows everything, then we fall + * back on acting directly. This means there is no way to intercept from + * the Tcl level. + */ + + return SharedTextObjCmd(sharedPtr, interp, objc+1, objv-1); +} + +/* + *---------------------------------------------------------------------- + * + * CountIndices -- + * + * This function implements most of the functionality of the "count" + * widget command. + * + * Note that 'textPtr' is only used if we need to check for elided + * attributes, i.e. if type is COUNT_DISPLAY_INDICES or + * COUNT_DISPLAY_CHARS + * + * Results: + * Returns the number of characters in the range. + * + * Side effects: + * None. + * + *---------------------------------------------------------------------- + */ + +static int +CountIndices( + const TkText *textPtr, /* Overall information about text widget. */ + const TkTextIndex *indexPtr1, + /* Index describing location of first + * character to delete. */ + const TkTextIndex *indexPtr2, + /* Index describing location of last character + * to delete. NULL means just delete the one + * character given by indexPtr1. */ + TkTextCountType type) /* The kind of indices to count. */ +{ + /* + * Order the starting and stopping indices. + */ + + int compare = TkTextIndexCmp(indexPtr1, indexPtr2); + + if (compare == 0) { + return 0; + } else if (compare > 0) { + return -TkTextIndexCount(textPtr, indexPtr2, indexPtr1, type); + } else { + return TkTextIndexCount(textPtr, indexPtr1, indexPtr2, type); + } +} + +/* + *---------------------------------------------------------------------- + * + * DeleteIndexRange -- + * + * This function implements most of the functionality of the "delete" + * widget command. + * + * Results: + * Returns a standard Tcl result, currently always TCL_OK. + * + * Side effects: + * Characters and other entities (windows, images) get deleted from the + * text. + * + * If 'viewUpdate' is true, we may adjust the window contents' + * y-position, and scrollbar setting. + * + * If 'viewUpdate' is false, true we can guarantee that textPtr->topIndex + * points to a valid TkTextLine after this function returns. However, if + * 'viewUpdate' is false, then there is no such guarantee (since + * topIndex.linePtr can be garbage). The caller is expected to take + * actions to ensure the topIndex is validated before laying out the + * window again. + * + *---------------------------------------------------------------------- + */ + +static int +DeleteIndexRange( + TkSharedText *sharedTextPtr,/* Shared portion of peer widgets. */ + TkText *textPtr, /* Overall information about text widget. */ + const TkTextIndex *indexPtr1, + /* Index describing location of first + * character (or other entity) to delete. */ + const TkTextIndex *indexPtr2, + /* Index describing location of last + * character (or other entity) to delete. + * NULL means just delete the one character + * given by indexPtr1. */ + int viewUpdate) /* Update vertical view if set. */ +{ + int line1, line2; + TkTextIndex index1, index2; + TkText *tPtr; + int *lineAndByteIndex; + int resetViewCount; + int pixels[2*PIXEL_CLIENTS]; + + if (sharedTextPtr == NULL) { + sharedTextPtr = textPtr->sharedTextPtr; + } + + /* + * Prepare the starting and stopping indices. + */ + + index1 = *indexPtr1; + if (indexPtr2 != NULL) { + index2 = *indexPtr2; + } else { + index2 = index1; + TkTextIndexForwChars(NULL, &index2, 1, &index2, COUNT_INDICES); + } + + /* + * Make sure there's really something to delete. + */ + + if (TkTextIndexCmp(&index1, &index2) >= 0) { + return TCL_OK; + } + + /* + * The code below is ugly, but it's needed to make sure there is always a + * dummy empty line at the end of the text. If the final newline of the + * file (just before the dummy line) is being deleted, then back up index + * to just before the newline. If there is a newline just before the first + * character being deleted, then back up the first index too. The idea is + * that a deletion involving a range starting at a line start and + * including the final \n (i.e. index2 is "end") is an attempt to delete + * complete lines, so the \n before the deleted block shall become the new + * final \n. Furthermore, remove any tags that are present on the newline + * that isn't going to be deleted after all (this simulates deleting the + * newline and then adding a "clean" one back again). Note that index1 and + * index2 might now be equal again which means that no text will be + * deleted but tags might be removed. + */ + + line1 = TkBTreeLinesTo(textPtr, index1.linePtr); + line2 = TkBTreeLinesTo(textPtr, index2.linePtr); + if (line2 == TkBTreeNumLines(sharedTextPtr->tree, textPtr)) { + TkTextTag **arrayPtr; + int arraySize, i; + TkTextIndex oldIndex2; + + oldIndex2 = index2; + TkTextIndexBackChars(NULL, &oldIndex2, 1, &index2, COUNT_INDICES); + line2--; + if ((index1.byteIndex == 0) && (line1 != 0)) { + TkTextIndexBackChars(NULL, &index1, 1, &index1, COUNT_INDICES); + line1--; + } + arrayPtr = TkBTreeGetTags(&index2, NULL, &arraySize); + if (arrayPtr != NULL) { + for (i = 0; i < arraySize; i++) { + TkBTreeTag(&index2, &oldIndex2, arrayPtr[i], 0); + } + ckfree(arrayPtr); + } + } + + if (line1 < line2) { + /* + * We are deleting more than one line. For speed, we remove all tags + * from the range first. If we don't do this, the code below can (when + * there are many tags) grow non-linearly in execution time. + */ + + Tcl_HashSearch search; + Tcl_HashEntry *hPtr; + int i; + + for (i=0, hPtr=Tcl_FirstHashEntry(&sharedTextPtr->tagTable, &search); + hPtr != NULL; i++, hPtr = Tcl_NextHashEntry(&search)) { + TkTextTag *tagPtr = Tcl_GetHashValue(hPtr); + + TkBTreeTag(&index1, &index2, tagPtr, 0); + } + + /* + * Special case for the sel tag which is not in the hash table. We + * need to do this once for each peer text widget. + */ + + for (tPtr = sharedTextPtr->peers; tPtr != NULL ; + tPtr = tPtr->next) { + if (TkBTreeTag(&index1, &index2, tPtr->selTagPtr, 0)) { + /* + * Send an event that the selection changed. This is + * equivalent to: + * event generate $textWidget <<Selection>> + */ + + TkTextSelectionEvent(textPtr); + tPtr->abortSelections = 1; + } + } + } + + /* + * Tell the display what's about to happen so it can discard obsolete + * display information, then do the deletion. Also, if the deletion + * involves the top line on the screen, then we have to reset the view + * (the deletion will invalidate textPtr->topIndex). Compute what the new + * first character will be, then do the deletion, then reset the view. + */ + + TkTextChanged(sharedTextPtr, NULL, &index1, &index2); + + resetViewCount = 0; + if (sharedTextPtr->refCount > PIXEL_CLIENTS) { + lineAndByteIndex = ckalloc(sizeof(int) * 2 * sharedTextPtr->refCount); + } else { + lineAndByteIndex = pixels; + } + for (tPtr = sharedTextPtr->peers; tPtr != NULL ; tPtr = tPtr->next) { + int line = 0; + int byteIndex = 0; + int resetView = 0; + + if (TkTextIndexCmp(&index2, &tPtr->topIndex) >= 0) { + if (TkTextIndexCmp(&index1, &tPtr->topIndex) <= 0) { + /* + * Deletion range straddles topIndex: use the beginning of the + * range as the new topIndex. + */ + + resetView = 1; + line = line1; + byteIndex = index1.byteIndex; + } else if (index1.linePtr == tPtr->topIndex.linePtr) { + /* + * Deletion range starts on top line but after topIndex. Use + * the current topIndex as the new one. + */ + + resetView = 1; + line = line1; + byteIndex = tPtr->topIndex.byteIndex; + } else { + /* + * Deletion range starts after the top line. This peers's view + * will not need to be reset. Nothing to do. + */ + } + } else if (index2.linePtr == tPtr->topIndex.linePtr) { + /* + * Deletion range ends on top line but before topIndex. Figure out + * what will be the new character index for the character + * currently pointed to by topIndex. + */ + + resetView = 1; + line = line2; + byteIndex = tPtr->topIndex.byteIndex; + if (index1.linePtr != index2.linePtr) { + byteIndex -= index2.byteIndex; + } else { + byteIndex -= (index2.byteIndex - index1.byteIndex); + } + } else { + /* + * Deletion range ends before the top line. This peers's view + * will not need to be reset. Nothing to do. + */ + } + if (resetView) { + lineAndByteIndex[resetViewCount] = line; + lineAndByteIndex[resetViewCount+1] = byteIndex; + } else { + lineAndByteIndex[resetViewCount] = -1; + } + resetViewCount += 2; + } + + /* + * Push the deletion on the undo stack if something was actually deleted. + */ + + if (TkTextIndexCmp(&index1, &index2) < 0) { + if (sharedTextPtr->undo) { + Tcl_Obj *get; + + if (sharedTextPtr->autoSeparators + && (sharedTextPtr->lastEditMode != TK_TEXT_EDIT_DELETE)) { + TkUndoInsertUndoSeparator(sharedTextPtr->undoStack); + } + + sharedTextPtr->lastEditMode = TK_TEXT_EDIT_DELETE; + + get = TextGetText(textPtr, &index1, &index2, 0); + TextPushUndoAction(textPtr, get, 0, &index1, &index2); + } + sharedTextPtr->stateEpoch++; + + TkBTreeDeleteIndexRange(sharedTextPtr->tree, &index1, &index2); + + UpdateDirtyFlag(sharedTextPtr); + } + + resetViewCount = 0; + for (tPtr = sharedTextPtr->peers; tPtr != NULL ; tPtr = tPtr->next) { + int line = lineAndByteIndex[resetViewCount]; + + if (line != -1) { + int byteIndex = lineAndByteIndex[resetViewCount+1]; + TkTextIndex indexTmp; + + if (tPtr == textPtr) { + if (viewUpdate) { + /* + * line cannot be before -startline of textPtr because + * this line corresponds to an index which is necessarily + * between "1.0" and "end" relative to textPtr. + * Therefore no need to clamp line to the -start/-end + * range. + */ + + TkTextMakeByteIndex(sharedTextPtr->tree, textPtr, line, + byteIndex, &indexTmp); + TkTextSetYView(tPtr, &indexTmp, 0); + } + } else { + TkTextMakeByteIndex(sharedTextPtr->tree, tPtr, line, + byteIndex, &indexTmp); + /* + * line may be before -startline of tPtr and must be + * clamped to -startline before providing it to + * TkTextSetYView otherwise lines before -startline + * would be displayed. + * There is no need to worry about -endline however, + * because the view will only be reset if the deletion + * involves the TOP line of the screen + */ + + if (tPtr->start != NULL) { + int start; + TkTextIndex indexStart; + + start = TkBTreeLinesTo(NULL, tPtr->start); + TkTextMakeByteIndex(sharedTextPtr->tree, NULL, start, + 0, &indexStart); + if (TkTextIndexCmp(&indexTmp, &indexStart) < 0) { + indexTmp = indexStart; + } + } + TkTextSetYView(tPtr, &indexTmp, 0); + } + } + resetViewCount += 2; + } + if (sharedTextPtr->refCount > PIXEL_CLIENTS) { + ckfree(lineAndByteIndex); + } + + if (line1 >= line2) { + /* + * Invalidate any selection retrievals in progress, assuming we didn't + * check for this case above. + */ + + for (tPtr = sharedTextPtr->peers; tPtr != NULL ; tPtr = tPtr->next) { + tPtr->abortSelections = 1; + } + } + + return TCL_OK; +} + +/* + *---------------------------------------------------------------------- + * + * TextFetchSelection -- + * + * This function is called back by Tk when the selection is requested by + * someone. It returns part or all of the selection in a buffer provided + * by the caller. + * + * Results: + * The return value is the number of non-NULL bytes stored at buffer. + * Buffer is filled (or partially filled) with a NULL-terminated string + * containing part or all of the selection, as given by offset and + * maxBytes. + * + * Side effects: + * None. + * + *---------------------------------------------------------------------- + */ + +static int +TextFetchSelection( + ClientData clientData, /* Information about text widget. */ + int offset, /* Offset within selection of first character + * to be returned. */ + char *buffer, /* Location in which to place selection. */ + int maxBytes) /* Maximum number of bytes to place at buffer, + * not including terminating NULL + * character. */ +{ + register TkText *textPtr = clientData; + TkTextIndex eof; + int count, chunkSize, offsetInSeg; + TkTextSearch search; + TkTextSegment *segPtr; + + if (!textPtr->exportSelection) { + return -1; + } + + /* + * Find the beginning of the next range of selected text. Note: if the + * selection is being retrieved in multiple pieces (offset != 0) and some + * modification has been made to the text that affects the selection then + * reject the selection request (make 'em start over again). + */ + + if (offset == 0) { + TkTextMakeByteIndex(textPtr->sharedTextPtr->tree, textPtr, 0, 0, + &textPtr->selIndex); + textPtr->abortSelections = 0; + } else if (textPtr->abortSelections) { + return 0; + } + TkTextMakeByteIndex(textPtr->sharedTextPtr->tree, textPtr, + TkBTreeNumLines(textPtr->sharedTextPtr->tree, textPtr), 0, &eof); + TkBTreeStartSearch(&textPtr->selIndex, &eof, textPtr->selTagPtr, &search); + if (!TkBTreeCharTagged(&textPtr->selIndex, textPtr->selTagPtr)) { + if (!TkBTreeNextTag(&search)) { + if (offset == 0) { + return -1; + } else { + return 0; + } + } + textPtr->selIndex = search.curIndex; + } + + /* + * Each iteration through the outer loop below scans one selected range. + * Each iteration through the inner loop scans one segment in the selected + * range. + */ + + count = 0; + while (1) { + /* + * Find the end of the current range of selected text. + */ + + if (!TkBTreeNextTag(&search)) { + Tcl_Panic("TextFetchSelection couldn't find end of range"); + } + + /* + * Copy information from character segments into the buffer until + * either we run out of space in the buffer or we get to the end of + * this range of text. + */ + + while (1) { + if (maxBytes == 0) { + goto fetchDone; + } + segPtr = TkTextIndexToSeg(&textPtr->selIndex, &offsetInSeg); + chunkSize = segPtr->size - offsetInSeg; + if (chunkSize > maxBytes) { + chunkSize = maxBytes; + } + if (textPtr->selIndex.linePtr == search.curIndex.linePtr) { + int leftInRange; + + leftInRange = search.curIndex.byteIndex + - textPtr->selIndex.byteIndex; + if (leftInRange < chunkSize) { + chunkSize = leftInRange; + if (chunkSize <= 0) { + break; + } + } + } + if ((segPtr->typePtr == &tkTextCharType) + && !TkTextIsElided(textPtr, &textPtr->selIndex, NULL)) { + memcpy(buffer, segPtr->body.chars + offsetInSeg, + (size_t) chunkSize); + buffer += chunkSize; + maxBytes -= chunkSize; + count += chunkSize; + } + TkTextIndexForwBytes(textPtr, &textPtr->selIndex, chunkSize, + &textPtr->selIndex); + } + + /* + * Find the beginning of the next range of selected text. + */ + + if (!TkBTreeNextTag(&search)) { + break; + } + textPtr->selIndex = search.curIndex; + } + + fetchDone: + *buffer = 0; + return count; +} + +/* + *---------------------------------------------------------------------- + * + * TkTextLostSelection -- + * + * This function is called back by Tk when the selection is grabbed away + * from a text widget. On Windows and Mac systems, we want to remember + * the selection for the next time the focus enters the window. On Unix, + * just remove the "sel" tag from everything in the widget. + * + * Results: + * None. + * + * Side effects: + * The "sel" tag is cleared from the window. + * + *---------------------------------------------------------------------- + */ + +void +TkTextLostSelection( + ClientData clientData) /* Information about text widget. */ +{ + register TkText *textPtr = clientData; + + if (TkpAlwaysShowSelection(textPtr->tkwin)) { + TkTextIndex start, end; + + if (!textPtr->exportSelection) { + return; + } + + /* + * On Windows and Mac systems, we want to remember the selection for + * the next time the focus enters the window. On Unix, just remove the + * "sel" tag from everything in the widget. + */ + + TkTextMakeByteIndex(textPtr->sharedTextPtr->tree, textPtr, + 0, 0, &start); + TkTextMakeByteIndex(textPtr->sharedTextPtr->tree, textPtr, + TkBTreeNumLines(textPtr->sharedTextPtr->tree, textPtr), + 0, &end); + TkTextRedrawTag(NULL, textPtr, &start, &end, textPtr->selTagPtr, 1); + TkBTreeTag(&start, &end, textPtr->selTagPtr, 0); + } + + /* + * Send an event that the selection changed. This is equivalent to: + * event generate $textWidget <<Selection>> + */ + + TkTextSelectionEvent(textPtr); + + textPtr->flags &= ~GOT_SELECTION; +} + +/* + *---------------------------------------------------------------------- + * + * TkTextSelectionEvent -- + * + * When anything relevant to the "sel" tag has been changed, call this + * function to generate a <<Selection>> event. + * + * Results: + * None. + * + * Side effects: + * If <<Selection>> bindings are present, they will trigger. + * + *---------------------------------------------------------------------- + */ + +void +TkTextSelectionEvent( + TkText *textPtr) +{ + /* + * Send an event that the selection changed. This is equivalent to: + * event generate $textWidget <<Selection>> + */ + + TkSendVirtualEvent(textPtr->tkwin, "Selection", NULL); +} + +/* + *---------------------------------------------------------------------- + * + * TextBlinkProc -- + * + * This function is called as a timer handler to blink the insertion + * cursor off and on. + * + * Results: + * None. + * + * Side effects: + * The cursor gets turned on or off, redisplay gets invoked, and this + * function reschedules itself. + * + *---------------------------------------------------------------------- + */ + +static void +TextBlinkProc( + ClientData clientData) /* Pointer to record describing text. */ +{ + register TkText *textPtr = clientData; + TkTextIndex index; + int x, y, w, h, charWidth; + + if ((textPtr->state == TK_TEXT_STATE_DISABLED) || + !(textPtr->flags & GOT_FOCUS) || (textPtr->insertOffTime == 0)) { + if (!(textPtr->flags & GOT_FOCUS) && + (textPtr->insertUnfocussed != TK_TEXT_INSERT_NOFOCUS_NONE)) { + /* + * The widget doesn't have the focus yet it is configured to + * display the cursor when it doesn't have the focus. Act now! + */ + + textPtr->flags |= INSERT_ON; + goto redrawInsert; + } + if ((textPtr->insertOffTime == 0) && !(textPtr->flags & INSERT_ON)) { + /* + * The widget was configured to have zero offtime while the + * insertion point was not displayed. We have to display it once. + */ + + textPtr->flags |= INSERT_ON; + goto redrawInsert; + } + return; + } + if (textPtr->flags & INSERT_ON) { + textPtr->flags &= ~INSERT_ON; + textPtr->insertBlinkHandler = Tcl_CreateTimerHandler( + textPtr->insertOffTime, TextBlinkProc, textPtr); + } else { + textPtr->flags |= INSERT_ON; + textPtr->insertBlinkHandler = Tcl_CreateTimerHandler( + textPtr->insertOnTime, TextBlinkProc, textPtr); + } + redrawInsert: + TkTextMarkSegToIndex(textPtr, textPtr->insertMarkPtr, &index); + if (TkTextIndexBbox(textPtr, &index, &x, &y, &w, &h, &charWidth) == 0) { + if (textPtr->insertCursorType) { + /* Block cursor */ + TkTextRedrawRegion(textPtr, x - textPtr->width / 2, y, + charWidth + textPtr->insertWidth / 2, h); + } else { + /* I-beam cursor */ + TkTextRedrawRegion(textPtr, x - textPtr->insertWidth / 2, y, + textPtr->insertWidth, h); + } + } +} + +/* + *---------------------------------------------------------------------- + * + * TextInsertCmd -- + * + * This function is invoked to process the "insert" and "replace" widget + * commands for text widgets. + * + * Results: + * A standard Tcl result. + * + * Side effects: + * See the user documentation. + * + * If 'viewUpdate' is true, we may adjust the window contents' + * y-position, and scrollbar setting. + * + *---------------------------------------------------------------------- + */ + +static int +TextInsertCmd( + TkSharedText *sharedTextPtr,/* Shared portion of peer widgets. */ + TkText *textPtr, /* Information about text widget. */ + Tcl_Interp *interp, /* Current interpreter. */ + int objc, /* Number of arguments. */ + Tcl_Obj *const objv[], /* Argument objects. */ + const TkTextIndex *indexPtr,/* Index at which to insert. */ + int viewUpdate) /* Update the view if set. */ +{ + TkTextIndex index1, index2; + int j; + + if (sharedTextPtr == NULL) { + sharedTextPtr = textPtr->sharedTextPtr; + } + + index1 = *indexPtr; + for (j = 0; j < objc; j += 2) { + /* + * Here we rely on this call to modify index1 if it is outside the + * acceptable range. In particular, if index1 is "end", it must be set + * to the last allowable index for insertion, otherwise subsequent tag + * insertions will fail. + */ + + int length = InsertChars(sharedTextPtr, textPtr, &index1, objv[j], + viewUpdate); + + if (objc > (j+1)) { + Tcl_Obj **tagNamePtrs; + TkTextTag **oldTagArrayPtr; + int numTags; + + TkTextIndexForwBytes(textPtr, &index1, length, &index2); + oldTagArrayPtr = TkBTreeGetTags(&index1, NULL, &numTags); + if (oldTagArrayPtr != NULL) { + int i; + + for (i = 0; i < numTags; i++) { + TkBTreeTag(&index1, &index2, oldTagArrayPtr[i], 0); + } + ckfree(oldTagArrayPtr); + } + if (Tcl_ListObjGetElements(interp, objv[j+1], &numTags, + &tagNamePtrs) != TCL_OK) { + return TCL_ERROR; + } else { + int i; + + for (i = 0; i < numTags; i++) { + const char *strTag = Tcl_GetString(tagNamePtrs[i]); + + TkBTreeTag(&index1, &index2, + TkTextCreateTag(textPtr, strTag, NULL), 1); + } + index1 = index2; + } + } + } + return TCL_OK; +} + +/* + *---------------------------------------------------------------------- + * + * TextSearchCmd -- + * + * This function is invoked to process the "search" widget command for + * text widgets. See the user documentation for details on what it does. + * + * Results: + * A standard Tcl result. + * + * Side effects: + * See the user documentation. + * + *---------------------------------------------------------------------- + */ + +static int +TextSearchCmd( + TkText *textPtr, /* Information about text widget. */ + Tcl_Interp *interp, /* Current interpreter. */ + int objc, /* Number of arguments. */ + Tcl_Obj *const objv[]) /* Argument objects. */ +{ + int i, argsLeft, code; + SearchSpec searchSpec; + + static const char *const switchStrings[] = { + "-hidden", + "--", "-all", "-backwards", "-count", "-elide", "-exact", "-forwards", + "-nocase", "-nolinestop", "-overlap", "-regexp", "-strictlimits", NULL + }; + enum SearchSwitches { + SEARCH_HIDDEN, + SEARCH_END, SEARCH_ALL, SEARCH_BACK, SEARCH_COUNT, SEARCH_ELIDE, + SEARCH_EXACT, SEARCH_FWD, SEARCH_NOCASE, + SEARCH_NOLINESTOP, SEARCH_OVERLAP, SEARCH_REGEXP, SEARCH_STRICTLIMITS + }; + + /* + * Set up the search specification, including the last 4 fields which are + * text widget specific. + */ + + searchSpec.exact = 1; + searchSpec.noCase = 0; + searchSpec.all = 0; + searchSpec.backwards = 0; + searchSpec.varPtr = NULL; + searchSpec.countPtr = NULL; + searchSpec.resPtr = NULL; + searchSpec.searchElide = 0; + searchSpec.noLineStop = 0; + searchSpec.overlap = 0; + searchSpec.strictLimits = 0; + searchSpec.numLines = + TkBTreeNumLines(textPtr->sharedTextPtr->tree, textPtr); + searchSpec.clientData = textPtr; + searchSpec.addLineProc = &TextSearchAddNextLine; + searchSpec.foundMatchProc = &TextSearchFoundMatch; + searchSpec.lineIndexProc = &TextSearchGetLineIndex; + + /* + * Parse switches and other arguments. + */ + + for (i=2 ; i<objc ; i++) { + int index; + + if (Tcl_GetString(objv[i])[0] != '-') { + break; + } + + if (Tcl_GetIndexFromObjStruct(NULL, objv[i], switchStrings, + sizeof(char *), "switch", 0, &index) != TCL_OK) { + /* + * Hide the -hidden option, generating the error description with + * the side effects of T_GIFO. + */ + + (void) Tcl_GetIndexFromObjStruct(interp, objv[i], switchStrings+1, + sizeof(char *), "switch", 0, &index); + return TCL_ERROR; + } + + switch ((enum SearchSwitches) index) { + case SEARCH_END: + i++; + goto endOfSwitchProcessing; + case SEARCH_ALL: + searchSpec.all = 1; + break; + case SEARCH_BACK: + searchSpec.backwards = 1; + break; + case SEARCH_COUNT: + if (i >= objc-1) { + Tcl_SetObjResult(interp, Tcl_NewStringObj( + "no value given for \"-count\" option", -1)); + Tcl_SetErrorCode(interp, "TK", "TEXT", "VALUE", NULL); + return TCL_ERROR; + } + i++; + + /* + * Assumption objv[i] isn't going to disappear on us during this + * function, which is fair. + */ + + searchSpec.varPtr = objv[i]; + break; + case SEARCH_ELIDE: + case SEARCH_HIDDEN: + searchSpec.searchElide = 1; + break; + case SEARCH_EXACT: + searchSpec.exact = 1; + break; + case SEARCH_FWD: + searchSpec.backwards = 0; + break; + case SEARCH_NOCASE: + searchSpec.noCase = 1; + break; + case SEARCH_NOLINESTOP: + searchSpec.noLineStop = 1; + break; + case SEARCH_OVERLAP: + searchSpec.overlap = 1; + break; + case SEARCH_STRICTLIMITS: + searchSpec.strictLimits = 1; + break; + case SEARCH_REGEXP: + searchSpec.exact = 0; + break; + default: + Tcl_Panic("unexpected switch fallthrough"); + } + } + endOfSwitchProcessing: + + argsLeft = objc - (i+2); + if ((argsLeft != 0) && (argsLeft != 1)) { + Tcl_WrongNumArgs(interp, 2, objv, + "?switches? pattern index ?stopIndex?"); + return TCL_ERROR; + } + + if (searchSpec.noLineStop && searchSpec.exact) { + Tcl_SetObjResult(interp, Tcl_NewStringObj( + "the \"-nolinestop\" option requires the \"-regexp\" option" + " to be present", -1)); + Tcl_SetErrorCode(interp, "TK", "TEXT", "SEARCH_USAGE", NULL); + return TCL_ERROR; + } + + if (searchSpec.overlap && !searchSpec.all) { + Tcl_SetObjResult(interp, Tcl_NewStringObj( + "the \"-overlap\" option requires the \"-all\" option" + " to be present", -1)); + Tcl_SetErrorCode(interp, "TK", "TEXT", "SEARCH_USAGE", NULL); + return TCL_ERROR; + } + + /* + * Scan through all of the lines of the text circularly, starting at the + * given index. 'objv[i]' is the pattern which may be an exact string or a + * regexp pattern depending on the flags set above. + */ + + code = SearchPerform(interp, &searchSpec, objv[i], objv[i+1], + (argsLeft == 1 ? objv[i+2] : NULL)); + if (code != TCL_OK) { + goto cleanup; + } + + /* + * Set the '-count' variable, if given. + */ + + if (searchSpec.varPtr != NULL && searchSpec.countPtr != NULL) { + Tcl_IncrRefCount(searchSpec.countPtr); + if (Tcl_ObjSetVar2(interp, searchSpec.varPtr, NULL, + searchSpec.countPtr, TCL_LEAVE_ERR_MSG) == NULL) { + code = TCL_ERROR; + goto cleanup; + } + } + + /* + * Set the result. + */ + + if (searchSpec.resPtr != NULL) { + Tcl_SetObjResult(interp, searchSpec.resPtr); + searchSpec.resPtr = NULL; + } + + cleanup: + if (searchSpec.countPtr != NULL) { + Tcl_DecrRefCount(searchSpec.countPtr); + } + if (searchSpec.resPtr != NULL) { + Tcl_DecrRefCount(searchSpec.resPtr); + } + return code; +} + +/* + *---------------------------------------------------------------------- + * + * TextSearchGetLineIndex -- + * + * Extract a row, text offset index position from an objPtr + * + * This means we ignore any embedded windows/images and elidden text + * (unless we are searching that). + * + * Results: + * Standard Tcl error code (with a message in the interpreter on error + * conditions). + * + * The offset placed in offsetPosPtr is a utf-8 char* byte index for + * exact searches, and a Unicode character index for regexp searches. + * + * The line number should start at zero (searches which wrap around + * assume the first line is numbered 0). + * + * Side effects: + * None. + * + *---------------------------------------------------------------------- + */ + +static int +TextSearchGetLineIndex( + Tcl_Interp *interp, /* For error messages. */ + Tcl_Obj *objPtr, /* Contains a textual index like "1.2" */ + SearchSpec *searchSpecPtr, /* Contains other search parameters. */ + int *linePosPtr, /* For returning the line number. */ + int *offsetPosPtr) /* For returning the text offset in the + * line. */ +{ + const TkTextIndex *indexPtr; + int line; + TkText *textPtr = searchSpecPtr->clientData; + + indexPtr = TkTextGetIndexFromObj(interp, textPtr, objPtr); + if (indexPtr == NULL) { + return TCL_ERROR; + } + + line = TkBTreeLinesTo(textPtr, indexPtr->linePtr); + if (line >= searchSpecPtr->numLines) { + TkTextLine *linePtr; + int count = 0; + TkTextSegment *segPtr; + + line = searchSpecPtr->numLines-1; + linePtr = TkBTreeFindLine(textPtr->sharedTextPtr->tree, textPtr, line); + + /* + * Count the number of bytes in this line. + */ + + for (segPtr=linePtr->segPtr ; segPtr!=NULL ; segPtr=segPtr->nextPtr) { + count += segPtr->size; + } + *offsetPosPtr = TextSearchIndexInLine(searchSpecPtr, linePtr, count); + } else { + *offsetPosPtr = TextSearchIndexInLine(searchSpecPtr, + indexPtr->linePtr, indexPtr->byteIndex); + } + + *linePosPtr = line; + + return TCL_OK; +} + +/* + *---------------------------------------------------------------------- + * + * TextSearchIndexInLine -- + * + * Find textual index of 'byteIndex' in the searchable characters of + * 'linePtr'. + * + * This means we ignore any embedded windows/images and elidden text + * (unless we are searching that). + * + * Results: + * The returned index is a utf-8 char* byte index for exact searches, and + * a Unicode character index for regexp searches. + * + * Side effects: + * None. + * + *---------------------------------------------------------------------- + */ + +static int +TextSearchIndexInLine( + const SearchSpec *searchSpecPtr, + /* Search parameters. */ + TkTextLine *linePtr, /* The line we're looking at. */ + int byteIndex) /* Index into the line. */ +{ + TkTextSegment *segPtr; + TkTextIndex curIndex; + int index, leftToScan; + TkText *textPtr = searchSpecPtr->clientData; + + index = 0; + curIndex.tree = textPtr->sharedTextPtr->tree; + curIndex.linePtr = linePtr; curIndex.byteIndex = 0; + for (segPtr = linePtr->segPtr, leftToScan = byteIndex; + leftToScan > 0; + curIndex.byteIndex += segPtr->size, segPtr = segPtr->nextPtr) { + if ((segPtr->typePtr == &tkTextCharType) && + (searchSpecPtr->searchElide + || !TkTextIsElided(textPtr, &curIndex, NULL))) { + if (leftToScan < segPtr->size) { + if (searchSpecPtr->exact) { + index += leftToScan; + } else { + index += Tcl_NumUtfChars(segPtr->body.chars, leftToScan); + } + } else if (searchSpecPtr->exact) { + index += segPtr->size; + } else { + index += Tcl_NumUtfChars(segPtr->body.chars, -1); + } + } + leftToScan -= segPtr->size; + } + return index; +} + +/* + *---------------------------------------------------------------------- + * + * TextSearchAddNextLine -- + * + * Adds a line from the text widget to the object 'theLine'. + * + * Results: + * A pointer to the TkTextLine corresponding to the given line, or NULL + * if there was no available line. + * + * Also 'lenPtr' (if non-NULL) is filled in with the total length of + * 'theLine' (not just what we added to it, but the length including what + * was already in there). This is in bytes for an exact search and in + * chars for a regexp search. + * + * Also 'extraLinesPtr' (if non-NULL) will have its value incremented by + * 1 for each additional logical line we have added because a newline is + * elided (this will only ever happen if we have chosen not to search + * elided text, of course). + * + * Side effects: + * Memory may be allocated or re-allocated for theLine's string + * representation. + * + *---------------------------------------------------------------------- + */ + +static ClientData +TextSearchAddNextLine( + int lineNum, /* Line we must add. */ + SearchSpec *searchSpecPtr, /* Search parameters. */ + Tcl_Obj *theLine, /* Object to append to. */ + int *lenPtr, /* For returning the total length. */ + int *extraLinesPtr) /* If non-NULL, will have its value + * incremented by the number of additional + * logical lines which are merged into this + * one by newlines being elided. */ +{ + TkTextLine *linePtr, *thisLinePtr; + TkTextIndex curIndex; + TkTextSegment *segPtr; + TkText *textPtr = searchSpecPtr->clientData; + int nothingYet = 1; + + /* + * Extract the text from the line. + */ + + linePtr = TkBTreeFindLine(textPtr->sharedTextPtr->tree, textPtr, lineNum); + if (linePtr == NULL) { + return NULL; + } + curIndex.tree = textPtr->sharedTextPtr->tree; + thisLinePtr = linePtr; + + while (thisLinePtr != NULL) { + int elideWraps = 0; + + curIndex.linePtr = thisLinePtr; + curIndex.byteIndex = 0; + for (segPtr = thisLinePtr->segPtr; segPtr != NULL; + curIndex.byteIndex += segPtr->size, segPtr = segPtr->nextPtr) { + if (!searchSpecPtr->searchElide + && TkTextIsElided(textPtr, &curIndex, NULL)) { + /* + * If we reach the end of the logical line, and if we have at + * least one character in the string, then we continue + * wrapping to the next logical line. If there are no + * characters yet, then the entire line of characters is + * elided and there's no need to complicate matters by + * wrapping - we'll look at the next line in due course. + */ + + if (segPtr->nextPtr == NULL && !nothingYet) { + elideWraps = 1; + } + continue; + } + if (segPtr->typePtr != &tkTextCharType) { + continue; + } + Tcl_AppendToObj(theLine, segPtr->body.chars, segPtr->size); + nothingYet = 0; + } + if (!elideWraps) { + break; + } + lineNum++; + if (lineNum >= searchSpecPtr->numLines) { + break; + } + thisLinePtr = TkBTreeNextLine(textPtr, thisLinePtr); + if (thisLinePtr != NULL && extraLinesPtr != NULL) { + /* + * Tell our caller we have an extra line merged in. + */ + + *extraLinesPtr = (*extraLinesPtr) + 1; + } + } + + /* + * If we're ignoring case, convert the line to lower case. There is no + * need to do this for regexp searches, since they handle a flag for this + * purpose. + */ + + if (searchSpecPtr->exact && searchSpecPtr->noCase) { + Tcl_SetObjLength(theLine, Tcl_UtfToLower(Tcl_GetString(theLine))); + } + + if (lenPtr != NULL) { + if (searchSpecPtr->exact) { + (void)Tcl_GetString(theLine); + *lenPtr = theLine->length; + } else { + *lenPtr = Tcl_GetCharLength(theLine); + } + } + return linePtr; +} + +/* + *---------------------------------------------------------------------- + * + * TextSearchFoundMatch -- + * + * Stores information from a successful search. + * + * Results: + * 1 if the information was stored, 0 if the position at which the match + * was found actually falls outside the allowable search region (and + * therefore the search is actually complete). + * + * Side effects: + * Memory may be allocated in the 'countPtr' and 'resPtr' fields of + * 'searchSpecPtr'. Each of those objects will have refCount zero and + * must eventually be freed or stored elsewhere as appropriate. + * + *---------------------------------------------------------------------- + */ + +static int +TextSearchFoundMatch( + int lineNum, /* Line on which match was found. */ + SearchSpec *searchSpecPtr, /* Search parameters. */ + ClientData clientData, /* Token returned by the 'addNextLineProc', + * TextSearchAddNextLine. May be NULL, in + * which we case we must generate it (from + * lineNum). */ + Tcl_Obj *theLine, /* Text from current line, only accessed for + * exact searches, and is allowed to be NULL + * for regexp searches. */ + int matchOffset, /* Offset of found item in utf-8 bytes for + * exact search, Unicode chars for regexp. */ + int matchLength) /* Length also in bytes/chars as per search + * type. */ +{ + int numChars; + int leftToScan; + TkTextIndex curIndex, foundIndex; + TkTextSegment *segPtr; + TkTextLine *linePtr; + TkText *textPtr = searchSpecPtr->clientData; + + if (lineNum == searchSpecPtr->stopLine) { + /* + * If the current index is on the wrong side of the stopIndex, then + * the item we just found is actually outside the acceptable range, + * and the search is over. + */ + + if (searchSpecPtr->backwards ^ + (matchOffset >= searchSpecPtr->stopOffset)) { + return 0; + } + } + + /* + * Calculate the character count, which may need augmenting if there are + * embedded windows or elidden text. + */ + + if (searchSpecPtr->exact) { + const char *startOfLine = Tcl_GetString(theLine); + + numChars = Tcl_NumUtfChars(startOfLine + matchOffset, matchLength); + } else { + numChars = matchLength; + } + + /* + * If we're using strict limits checking, ensure that the match with its + * full length fits inside the given range. + */ + + if (searchSpecPtr->strictLimits && lineNum == searchSpecPtr->stopLine) { + if (searchSpecPtr->backwards ^ + ((matchOffset + numChars) > searchSpecPtr->stopOffset)) { + return 0; + } + } + + /* + * The index information returned by the regular expression parser only + * considers textual information: it doesn't account for embedded windows, + * elided text (when we are not searching elided text) or any other + * non-textual info. Scan through the line's segments again to adjust both + * matchChar and matchCount. + * + * We will walk through the segments of this line until we have either + * reached the end of the match or we have reached the end of the line. + */ + + linePtr = clientData; + if (linePtr == NULL) { + linePtr = TkBTreeFindLine(textPtr->sharedTextPtr->tree, textPtr, + lineNum); + } + + curIndex.tree = textPtr->sharedTextPtr->tree; + + /* + * Find the starting point. + */ + + leftToScan = matchOffset; + while (1) { + curIndex.linePtr = linePtr; + curIndex.byteIndex = 0; + + /* + * Note that we allow leftToScan to be zero because we want to skip + * over any preceding non-textual items. + */ + + for (segPtr = linePtr->segPtr; leftToScan >= 0 && segPtr; + segPtr = segPtr->nextPtr) { + if (segPtr->typePtr != &tkTextCharType) { + matchOffset += segPtr->size; + } else if (!searchSpecPtr->searchElide + && TkTextIsElided(textPtr, &curIndex, NULL)) { + if (searchSpecPtr->exact) { + matchOffset += segPtr->size; + } else { + matchOffset += Tcl_NumUtfChars(segPtr->body.chars, -1); + } + } else { + if (searchSpecPtr->exact) { + leftToScan -= segPtr->size; + } else { + leftToScan -= Tcl_NumUtfChars(segPtr->body.chars, -1); + } + } + curIndex.byteIndex += segPtr->size; + } + if (segPtr == NULL && leftToScan >= 0) { + /* + * This will only happen if we are eliding newlines. + */ + + linePtr = TkBTreeNextLine(textPtr, linePtr); + if (linePtr == NULL) { + /* + * If we reach the end of the text, we have a serious problem, + * unless there's actually nothing left to look for. + */ + + if (leftToScan == 0) { + break; + } else { + Tcl_Panic("Reached end of text in a match"); + } + } + + /* + * We've wrapped to the beginning of the next logical line, which + * has been merged with the previous one whose newline was elided. + */ + + lineNum++; + matchOffset = 0; + } else { + break; + } + } + + /* + * Calculate and store the found index in the result. + */ + + if (searchSpecPtr->exact) { + TkTextMakeByteIndex(textPtr->sharedTextPtr->tree, textPtr, lineNum, + matchOffset, &foundIndex); + } else { + TkTextMakeCharIndex(textPtr->sharedTextPtr->tree, textPtr, lineNum, + matchOffset, &foundIndex); + } + + if (searchSpecPtr->all) { + if (searchSpecPtr->resPtr == NULL) { + searchSpecPtr->resPtr = Tcl_NewObj(); + } + Tcl_ListObjAppendElement(NULL, searchSpecPtr->resPtr, + TkTextNewIndexObj(textPtr, &foundIndex)); + } else { + searchSpecPtr->resPtr = TkTextNewIndexObj(textPtr, &foundIndex); + } + + /* + * Find the end point. Here 'leftToScan' could be negative already as a + * result of the above loop if the segment we reached spanned the start of + * the string. When we add matchLength it will become non-negative. + */ + + for (leftToScan += matchLength; leftToScan > 0; + curIndex.byteIndex += segPtr->size, segPtr = segPtr->nextPtr) { + if (segPtr == NULL) { + /* + * We are on the next line - this of course should only ever + * happen with searches which have matched across multiple lines. + */ + + linePtr = TkBTreeNextLine(textPtr, linePtr); + segPtr = linePtr->segPtr; + curIndex.linePtr = linePtr; curIndex.byteIndex = 0; + } + if (segPtr->typePtr != &tkTextCharType) { + /* + * Anything we didn't count in the search needs adding. + */ + + numChars += segPtr->size; + continue; + } else if (!searchSpecPtr->searchElide + && TkTextIsElided(textPtr, &curIndex, NULL)) { + numChars += Tcl_NumUtfChars(segPtr->body.chars, -1); + continue; + } + if (searchSpecPtr->exact) { + leftToScan -= segPtr->size; + } else { + leftToScan -= Tcl_NumUtfChars(segPtr->body.chars, -1); + } + } + + /* + * Now store the count result, if it is wanted. + */ + + if (searchSpecPtr->varPtr != NULL) { + Tcl_Obj *tmpPtr = Tcl_NewIntObj(numChars); + if (searchSpecPtr->all) { + if (searchSpecPtr->countPtr == NULL) { + searchSpecPtr->countPtr = Tcl_NewObj(); + } + Tcl_ListObjAppendElement(NULL, searchSpecPtr->countPtr, tmpPtr); + } else { + searchSpecPtr->countPtr = tmpPtr; + } + } + return 1; +} + +/* + *---------------------------------------------------------------------- + * + * TkTextGetTabs -- + * + * Parses a string description of a set of tab stops. + * + * Results: + * The return value is a pointer to a malloc'ed structure holding parsed + * information about the tab stops. If an error occurred then the return + * value is NULL and an error message is left in the interp's result. + * + * Side effects: + * Memory is allocated for the structure that is returned. It is up to + * the caller to free this structure when it is no longer needed. + * + *---------------------------------------------------------------------- + */ + +TkTextTabArray * +TkTextGetTabs( + Tcl_Interp *interp, /* Used for error reporting. */ + TkText *textPtr, /* Information about the text widget. */ + Tcl_Obj *stringPtr) /* Description of the tab stops. See the text + * manual entry for details. */ +{ + int objc, i, count; + Tcl_Obj **objv; + TkTextTabArray *tabArrayPtr; + TkTextTab *tabPtr; + Tcl_UniChar ch; + double prevStop, lastStop; + /* + * Map these strings to TkTextTabAlign values. + */ + static const char *const tabOptionStrings[] = { + "left", "right", "center", "numeric", NULL + }; + + if (Tcl_ListObjGetElements(interp, stringPtr, &objc, &objv) != TCL_OK) { + return NULL; + } + + /* + * First find out how many entries we need to allocate in the tab array. + */ + + count = 0; + for (i = 0; i < objc; i++) { + char c = Tcl_GetString(objv[i])[0]; + + if ((c != 'l') && (c != 'r') && (c != 'c') && (c != 'n')) { + count++; + } + } + + /* + * Parse the elements of the list one at a time to fill in the array. + */ + + tabArrayPtr = ckalloc(sizeof(TkTextTabArray) + + (count - 1) * sizeof(TkTextTab)); + tabArrayPtr->numTabs = 0; + prevStop = 0.0; + lastStop = 0.0; + for (i = 0, tabPtr = &tabArrayPtr->tabs[0]; i < objc; i++, tabPtr++) { + int index; + + /* + * This will round fractional pixels above 0.5 upwards, and otherwise + * downwards, to find the right integer pixel position. + */ + + if (Tk_GetPixelsFromObj(interp, textPtr->tkwin, objv[i], + &tabPtr->location) != TCL_OK) { + goto error; + } + + if (tabPtr->location <= 0) { + Tcl_SetObjResult(interp, Tcl_ObjPrintf( + "tab stop \"%s\" is not at a positive distance", + Tcl_GetString(objv[i]))); + Tcl_SetErrorCode(interp, "TK", "VALUE", "TAB_STOP", NULL); + goto error; + } + + prevStop = lastStop; + if (Tk_GetDoublePixelsFromObj(interp, textPtr->tkwin, objv[i], + &lastStop) != TCL_OK) { + goto error; + } + + if (i > 0 && (tabPtr->location <= (tabPtr-1)->location)) { + /* + * This tab is actually to the left of the previous one, which is + * illegal. + */ + +#ifdef _TK_ALLOW_DECREASING_TABS + /* + * Force the tab to be a typical character width to the right of + * the previous one, and update the 'lastStop' with the changed + * position. + */ + + if (textPtr->charWidth > 0) { + tabPtr->location = (tabPtr-1)->location + textPtr->charWidth; + } else { + tabPtr->location = (tabPtr-1)->location + 8; + } + lastStop = tabPtr->location; +#else + Tcl_SetObjResult(interp, Tcl_ObjPrintf( + "tabs must be monotonically increasing, but \"%s\" is " + "smaller than or equal to the previous tab", + Tcl_GetString(objv[i]))); + Tcl_SetErrorCode(interp, "TK", "VALUE", "TAB_STOP", NULL); + goto error; +#endif /* _TK_ALLOW_DECREASING_TABS */ + } + + tabArrayPtr->numTabs++; + + /* + * See if there is an explicit alignment in the next list element. + * Otherwise just use "left". + */ + + tabPtr->alignment = LEFT; + if ((i+1) == objc) { + continue; + } + + /* + * There may be a more efficient way of getting this. + */ + + Tcl_UtfToUniChar(Tcl_GetString(objv[i+1]), &ch); + if (!Tcl_UniCharIsAlpha(ch)) { + continue; + } + i += 1; + + if (Tcl_GetIndexFromObjStruct(interp, objv[i], tabOptionStrings, + sizeof(char *), "tab alignment", 0, &index) != TCL_OK) { + goto error; + } + tabPtr->alignment = (TkTextTabAlign) index; + } + + /* + * For when we need to interpolate tab stops, store these two so we know + * the tab stop size to very high precision. With the above checks, we can + * guarantee that tabIncrement is strictly positive here. + */ + + tabArrayPtr->lastTab = lastStop; + tabArrayPtr->tabIncrement = lastStop - prevStop; + + return tabArrayPtr; + + error: + ckfree(tabArrayPtr); + return NULL; +} + +/* + *---------------------------------------------------------------------- + * + * TextDumpCmd -- + * + * Return information about the text, tags, marks, and embedded windows + * and images in a text widget. See the man page for the description of + * the text dump operation for all the details. + * + * Results: + * A standard Tcl result. + * + * Side effects: + * Memory is allocated for the result, if needed (standard Tcl result + * side effects). + * + *---------------------------------------------------------------------- + */ + +static int +TextDumpCmd( + register TkText *textPtr, /* Information about text widget. */ + Tcl_Interp *interp, /* Current interpreter. */ + int objc, /* Number of arguments. */ + Tcl_Obj *const objv[]) /* Argument objects. Someone else has already + * parsed this command enough to know that + * objv[1] is "dump". */ +{ + TkTextIndex index1, index2; + int arg; + int lineno; /* Current line number. */ + int what = 0; /* bitfield to select segment types. */ + int atEnd; /* True if dumping up to logical end. */ + TkTextLine *linePtr; + Tcl_Obj *command = NULL; /* Script callback to apply to segments. */ +#define TK_DUMP_TEXT 0x1 +#define TK_DUMP_MARK 0x2 +#define TK_DUMP_TAG 0x4 +#define TK_DUMP_WIN 0x8 +#define TK_DUMP_IMG 0x10 +#define TK_DUMP_ALL (TK_DUMP_TEXT|TK_DUMP_MARK|TK_DUMP_TAG| \ + TK_DUMP_WIN|TK_DUMP_IMG) + static const char *const optStrings[] = { + "-all", "-command", "-image", "-mark", "-tag", "-text", "-window", + NULL + }; + enum opts { + DUMP_ALL, DUMP_CMD, DUMP_IMG, DUMP_MARK, DUMP_TAG, DUMP_TXT, DUMP_WIN + }; + + for (arg=2 ; arg < objc ; arg++) { + int index; + if (Tcl_GetString(objv[arg])[0] != '-') { + break; + } + if (Tcl_GetIndexFromObjStruct(interp, objv[arg], optStrings, + sizeof(char *), "option", 0, &index) != TCL_OK) { + return TCL_ERROR; + } + switch ((enum opts) index) { + case DUMP_ALL: + what = TK_DUMP_ALL; + break; + case DUMP_TXT: + what |= TK_DUMP_TEXT; + break; + case DUMP_TAG: + what |= TK_DUMP_TAG; + break; + case DUMP_MARK: + what |= TK_DUMP_MARK; + break; + case DUMP_IMG: + what |= TK_DUMP_IMG; + break; + case DUMP_WIN: + what |= TK_DUMP_WIN; + break; + case DUMP_CMD: + arg++; + if (arg >= objc) { + goto wrongArgs; + } + command = objv[arg]; + break; + default: + Tcl_Panic("unexpected switch fallthrough"); + } + } + if (arg >= objc || arg+2 < objc) { + wrongArgs: + Tcl_SetObjResult(interp, Tcl_ObjPrintf( + "Usage: %s dump ?-all -image -text -mark -tag -window? " + "?-command script? index ?index2?", Tcl_GetString(objv[0]))); + Tcl_SetErrorCode(interp, "TCL", "WRONGARGS", NULL); + return TCL_ERROR; + } + if (what == 0) { + what = TK_DUMP_ALL; + } + if (TkTextGetObjIndex(interp, textPtr, objv[arg], &index1) != TCL_OK) { + return TCL_ERROR; + } + arg++; + atEnd = 0; + if (objc == arg) { + TkTextIndexForwChars(NULL, &index1, 1, &index2, COUNT_INDICES); + } else { + int length; + const char *str; + + if (TkTextGetObjIndex(interp, textPtr, objv[arg], &index2) != TCL_OK) { + return TCL_ERROR; + } + str = Tcl_GetString(objv[arg]); + length = objv[arg]->length; + if (strncmp(str, "end", (unsigned) length) == 0) { + atEnd = 1; + } + } + if (TkTextIndexCmp(&index1, &index2) >= 0) { + return TCL_OK; + } + lineno = TkBTreeLinesTo(textPtr, index1.linePtr); + if (index1.linePtr == index2.linePtr) { + DumpLine(interp, textPtr, what, index1.linePtr, + index1.byteIndex, index2.byteIndex, lineno, command); + } else { + int textChanged; + int lineend = TkBTreeLinesTo(textPtr, index2.linePtr); + int endByteIndex = index2.byteIndex; + + textChanged = DumpLine(interp, textPtr, what, index1.linePtr, + index1.byteIndex, 32000000, lineno, command); + if (textChanged) { + if (textPtr->flags & DESTROYED) { + return TCL_OK; + } + linePtr = TkBTreeFindLine(textPtr->sharedTextPtr->tree, + textPtr, lineno); + textChanged = 0; + } else { + linePtr = index1.linePtr; + } + while ((linePtr = TkBTreeNextLine(textPtr, linePtr)) != NULL) { + lineno++; + if (lineno == lineend) { + break; + } + textChanged = DumpLine(interp, textPtr, what, linePtr, 0, + 32000000, lineno, command); + if (textChanged) { + if (textPtr->flags & DESTROYED) { + return TCL_OK; + } + linePtr = TkBTreeFindLine(textPtr->sharedTextPtr->tree, + textPtr, lineno); + textChanged = 0; + } + } + if (linePtr != NULL) { + DumpLine(interp, textPtr, what, linePtr, 0, endByteIndex, lineno, + command); + if (textPtr->flags & DESTROYED) { + return TCL_OK; + } + } + } + + /* + * Special case to get the leftovers hiding at the end mark. + */ + + if (atEnd) { + if (textPtr->flags & DESTROYED) { + return TCL_OK; + } + + /* + * Re-get the end index, in case it has changed. + */ + + if (TkTextGetObjIndex(interp, textPtr, objv[arg], &index2) != TCL_OK) { + return TCL_ERROR; + } + DumpLine(interp, textPtr, what & ~TK_DUMP_TEXT, index2.linePtr, + 0, 1, lineno, command); + } + return TCL_OK; +} + +/* + *---------------------------------------------------------------------- + * + * DumpLine + * + * Return information about a given text line from character position + * "start" up to, but not including, "end". + * + * Results: + * Returns 1 if the command callback made any changes to the text widget + * which will have invalidated internal structures such as TkTextSegment, + * TkTextIndex, pointers. Our caller can then take action to recompute + * such entities. Returns 0 otherwise. + * + * Side effects: + * None, but see DumpSegment which can have arbitrary side-effects + * + *---------------------------------------------------------------------- + */ + +static int +DumpLine( + Tcl_Interp *interp, + TkText *textPtr, + int what, /* Bit flags to select segment types. */ + TkTextLine *linePtr, /* The current line. */ + int startByte, int endByte, /* Byte range to dump. */ + int lineno, /* Line number for indices dump. */ + Tcl_Obj *command) /* Script to apply to the segment. */ +{ + TkTextSegment *segPtr; + TkTextIndex index; + int offset = 0, textChanged = 0; + + /* + * Must loop through line looking at its segments. + * character + * toggleOn, toggleOff + * mark + * image + * window + */ + + segPtr = linePtr->segPtr; + while ((offset < endByte) && (segPtr != NULL)) { + int lineChanged = 0; + int currentSize = segPtr->size; + + if ((what & TK_DUMP_TEXT) && (segPtr->typePtr == &tkTextCharType) && + (offset + currentSize > startByte)) { + int last = currentSize; /* Index of last char in seg. */ + int first = 0; /* Index of first char in seg. */ + + if (offset + currentSize > endByte) { + last = endByte - offset; + } + if (startByte > offset) { + first = startByte - offset; + } + if (last != currentSize) { + /* + * To avoid modifying the string in place we copy over just + * the segment that we want. Since DumpSegment can modify the + * text, we could not confidently revert the modification + * here. + */ + + int length = last - first; + char *range = ckalloc(length + 1); + + memcpy(range, segPtr->body.chars + first, length); + range[length] = '\0'; + + TkTextMakeByteIndex(textPtr->sharedTextPtr->tree, textPtr, + lineno, offset + first, &index); + lineChanged = DumpSegment(textPtr, interp, "text", range, + command, &index, what); + ckfree(range); + } else { + TkTextMakeByteIndex(textPtr->sharedTextPtr->tree, textPtr, + lineno, offset + first, &index); + lineChanged = DumpSegment(textPtr, interp, "text", + segPtr->body.chars + first, command, &index, what); + } + } else if ((offset >= startByte)) { + if ((what & TK_DUMP_MARK) + && (segPtr->typePtr == &tkTextLeftMarkType + || segPtr->typePtr == &tkTextRightMarkType)) { + const char *name; + TkTextMark *markPtr = &segPtr->body.mark; + + if (segPtr == textPtr->insertMarkPtr) { + name = "insert"; + } else if (segPtr == textPtr->currentMarkPtr) { + name = "current"; + } else if (markPtr->hPtr == NULL) { + name = NULL; + lineChanged = 0; + } else { + name = Tcl_GetHashKey(&textPtr->sharedTextPtr->markTable, + markPtr->hPtr); + } + if (name != NULL) { + TkTextMakeByteIndex(textPtr->sharedTextPtr->tree, textPtr, + lineno, offset, &index); + lineChanged = DumpSegment(textPtr, interp, "mark", name, + command, &index, what); + } + } else if ((what & TK_DUMP_TAG) && + (segPtr->typePtr == &tkTextToggleOnType)) { + TkTextMakeByteIndex(textPtr->sharedTextPtr->tree, textPtr, + lineno, offset, &index); + lineChanged = DumpSegment(textPtr, interp, "tagon", + segPtr->body.toggle.tagPtr->name, command, &index, + what); + } else if ((what & TK_DUMP_TAG) && + (segPtr->typePtr == &tkTextToggleOffType)) { + TkTextMakeByteIndex(textPtr->sharedTextPtr->tree, textPtr, + lineno, offset, &index); + lineChanged = DumpSegment(textPtr, interp, "tagoff", + segPtr->body.toggle.tagPtr->name, command, &index, + what); + } else if ((what & TK_DUMP_IMG) && + (segPtr->typePtr == &tkTextEmbImageType)) { + TkTextEmbImage *eiPtr = &segPtr->body.ei; + const char *name = (eiPtr->name == NULL) ? "" : eiPtr->name; + + TkTextMakeByteIndex(textPtr->sharedTextPtr->tree, textPtr, + lineno, offset, &index); + lineChanged = DumpSegment(textPtr, interp, "image", name, + command, &index, what); + } else if ((what & TK_DUMP_WIN) && + (segPtr->typePtr == &tkTextEmbWindowType)) { + TkTextEmbWindow *ewPtr = &segPtr->body.ew; + const char *pathname; + + if (ewPtr->tkwin == (Tk_Window) NULL) { + pathname = ""; + } else { + pathname = Tk_PathName(ewPtr->tkwin); + } + TkTextMakeByteIndex(textPtr->sharedTextPtr->tree, textPtr, + lineno, offset, &index); + lineChanged = DumpSegment(textPtr, interp, "window", pathname, + command, &index, what); + } + } + + offset += currentSize; + if (lineChanged) { + TkTextSegment *newSegPtr; + int newOffset = 0; + + textChanged = 1; + + /* + * Our indices are no longer valid. + */ + + if (textPtr->flags & DESTROYED) { + return textChanged; + } + linePtr = TkBTreeFindLine(textPtr->sharedTextPtr->tree, + textPtr, lineno); + newSegPtr = linePtr->segPtr; + if (segPtr != newSegPtr) { + while ((newOffset < endByte) && (newOffset < offset) + && (newSegPtr != NULL)) { + newOffset += currentSize; + newSegPtr = newSegPtr->nextPtr; + if (segPtr == newSegPtr) { + break; + } + } + if (segPtr != newSegPtr && newOffset == offset + && currentSize == 0) { + TkTextSegment *searchPtr = newSegPtr; + + while (searchPtr != NULL && searchPtr->size == 0) { + if (searchPtr == segPtr) { + newSegPtr = searchPtr; + break; + } + searchPtr = searchPtr->nextPtr; + } + } + segPtr = newSegPtr; + } + } + if (segPtr != NULL) { + segPtr = segPtr->nextPtr; + } + } + return textChanged; +} + +/* + *---------------------------------------------------------------------- + * + * DumpSegment + * + * Either append information about the current segment to the result, or + * make a script callback with that information as arguments. + * + * Results: + * Returns 1 if the command callback made any changes to the text widget + * which will have invalidated internal structures such as TkTextSegment, + * TkTextIndex, pointers. Our caller can then take action to recompute + * such entities. Returns 0 otherwise. + * + * Side effects: + * Either evals the callback or appends elements to the result string. + * The callback can have arbitrary side-effects. + * + *---------------------------------------------------------------------- + */ + +static int +DumpSegment( + TkText *textPtr, + Tcl_Interp *interp, + const char *key, /* Segment type key. */ + const char *value, /* Segment value. */ + Tcl_Obj *command, /* Script callback. */ + const TkTextIndex *index, /* index with line/byte position info. */ + int what) /* Look for TK_DUMP_INDEX bit. */ +{ + char buffer[TK_POS_CHARS]; + Tcl_Obj *values[3], *tuple; + + TkTextPrintIndex(textPtr, index, buffer); + values[0] = Tcl_NewStringObj(key, -1); + values[1] = Tcl_NewStringObj(value, -1); + values[2] = Tcl_NewStringObj(buffer, -1); + tuple = Tcl_NewListObj(3, values); + if (command == NULL) { + Tcl_ListObjAppendList(NULL, Tcl_GetObjResult(interp), tuple); + Tcl_DecrRefCount(tuple); + return 0; + } else { + int oldStateEpoch = TkBTreeEpoch(textPtr->sharedTextPtr->tree); + Tcl_DString buf; + int code; + + Tcl_DStringInit(&buf); + Tcl_DStringAppend(&buf, Tcl_GetString(command), -1); + Tcl_DStringAppend(&buf, " ", -1); + Tcl_DStringAppend(&buf, Tcl_GetString(tuple), -1); + code = Tcl_EvalEx(interp, Tcl_DStringValue(&buf), -1, 0); + Tcl_DStringFree(&buf); + if (code != TCL_OK) { + Tcl_AddErrorInfo(interp, + "\n (segment dumping command executed by text)"); + Tcl_BackgroundException(interp, code); + } + Tcl_DecrRefCount(tuple); + return ((textPtr->flags & DESTROYED) || + TkBTreeEpoch(textPtr->sharedTextPtr->tree) != oldStateEpoch); + } +} + +/* + *---------------------------------------------------------------------- + * + * TextEditUndo -- + * + * Undo the last change. + * + * Results: + * None. + * + * Side effects: + * Apart from manipulating the undo and redo stacks, the state of the + * rest of the widget may also change (due to whatever is being undone). + * + *---------------------------------------------------------------------- + */ + +static int +TextEditUndo( + TkText *textPtr) /* Overall information about text widget. */ +{ + int status; + + if (!textPtr->sharedTextPtr->undo) { + return TCL_OK; + } + + /* + * Turn off the undo feature while we revert a compound action, setting + * the dirty handling mode to undo for the duration (unless it is + * 'fixed'). + */ + + textPtr->sharedTextPtr->undo = 0; + if (textPtr->sharedTextPtr->dirtyMode != TK_TEXT_DIRTY_FIXED) { + textPtr->sharedTextPtr->dirtyMode = TK_TEXT_DIRTY_UNDO; + } + + status = TkUndoRevert(textPtr->sharedTextPtr->undoStack); + + if (textPtr->sharedTextPtr->dirtyMode != TK_TEXT_DIRTY_FIXED) { + textPtr->sharedTextPtr->dirtyMode = TK_TEXT_DIRTY_NORMAL; + } + textPtr->sharedTextPtr->undo = 1; + + return status; +} + +/* + *---------------------------------------------------------------------- + * + * TextEditRedo -- + * + * Redo the last undone change. + * + * Results: + * None. + * + * Side effects: + * Apart from manipulating the undo and redo stacks, the state of the + * rest of the widget may also change (due to whatever is being redone). + * + *---------------------------------------------------------------------- + */ + +static int +TextEditRedo( + TkText *textPtr) /* Overall information about text widget. */ +{ + int status; + + if (!textPtr->sharedTextPtr->undo) { + return TCL_OK; + } + + /* + * Turn off the undo feature temporarily while we revert a previously + * undone compound action, setting the dirty handling mode to redo for the + * duration (unless it is 'fixed'). + */ + + textPtr->sharedTextPtr->undo = 0; + if (textPtr->sharedTextPtr->dirtyMode != TK_TEXT_DIRTY_FIXED) { + textPtr->sharedTextPtr->dirtyMode = TK_TEXT_DIRTY_REDO; + } + + status = TkUndoApply(textPtr->sharedTextPtr->undoStack); + + if (textPtr->sharedTextPtr->dirtyMode != TK_TEXT_DIRTY_FIXED) { + textPtr->sharedTextPtr->dirtyMode = TK_TEXT_DIRTY_NORMAL; + } + textPtr->sharedTextPtr->undo = 1; + return status; +} + +/* + *---------------------------------------------------------------------- + * + * TextEditCmd -- + * + * Handle the subcommands to "$text edit ...". See documentation for + * details. + * + * Results: + * None + * + * Side effects: + * None. + * + *---------------------------------------------------------------------- + */ + +static int +TextEditCmd( + TkText *textPtr, /* Information about text widget. */ + Tcl_Interp *interp, /* Current interpreter. */ + int objc, /* Number of arguments. */ + Tcl_Obj *const objv[]) /* Argument objects. */ +{ + int index, setModified, oldModified; + int canRedo = 0; + int canUndo = 0; + + static const char *const editOptionStrings[] = { + "canundo", "canredo", "modified", "redo", "reset", "separator", + "undo", NULL + }; + enum editOptions { + EDIT_CANUNDO, EDIT_CANREDO, EDIT_MODIFIED, EDIT_REDO, EDIT_RESET, + EDIT_SEPARATOR, EDIT_UNDO + }; + + if (objc < 3) { + Tcl_WrongNumArgs(interp, 2, objv, "option ?arg ...?"); + return TCL_ERROR; + } + + if (Tcl_GetIndexFromObjStruct(interp, objv[2], editOptionStrings, + sizeof(char *), "edit option", 0, &index) != TCL_OK) { + return TCL_ERROR; + } + + switch ((enum editOptions) index) { + case EDIT_CANREDO: + if (objc != 3) { + Tcl_WrongNumArgs(interp, 3, objv, NULL); + return TCL_ERROR; + } + if (textPtr->sharedTextPtr->undo) { + canRedo = TkUndoCanRedo(textPtr->sharedTextPtr->undoStack); + } + Tcl_SetObjResult(interp, Tcl_NewBooleanObj(canRedo)); + break; + case EDIT_CANUNDO: + if (objc != 3) { + Tcl_WrongNumArgs(interp, 3, objv, NULL); + return TCL_ERROR; + } + if (textPtr->sharedTextPtr->undo) { + canUndo = TkUndoCanUndo(textPtr->sharedTextPtr->undoStack); + } + Tcl_SetObjResult(interp, Tcl_NewBooleanObj(canUndo)); + break; + case EDIT_MODIFIED: + if (objc == 3) { + Tcl_SetObjResult(interp, + Tcl_NewBooleanObj(textPtr->sharedTextPtr->isDirty)); + return TCL_OK; + } else if (objc != 4) { + Tcl_WrongNumArgs(interp, 3, objv, "?boolean?"); + return TCL_ERROR; + } else if (Tcl_GetBooleanFromObj(interp, objv[3], + &setModified) != TCL_OK) { + return TCL_ERROR; + } + + /* + * Set or reset the dirty info, and trigger a Modified event. + */ + + setModified = setModified ? 1 : 0; + + oldModified = textPtr->sharedTextPtr->isDirty; + textPtr->sharedTextPtr->isDirty = setModified; + if (setModified) { + textPtr->sharedTextPtr->dirtyMode = TK_TEXT_DIRTY_FIXED; + } else { + textPtr->sharedTextPtr->dirtyMode = TK_TEXT_DIRTY_NORMAL; + } + + /* + * Only issue the <<Modified>> event if the flag actually changed. + * However, degree of modified-ness doesn't matter. [Bug 1799782] + */ + + if ((!oldModified) != (!setModified)) { + GenerateModifiedEvent(textPtr); + } + break; + case EDIT_REDO: + if (objc != 3) { + Tcl_WrongNumArgs(interp, 3, objv, NULL); + return TCL_ERROR; + } + canUndo = TkUndoCanUndo(textPtr->sharedTextPtr->undoStack); + if (TextEditRedo(textPtr)) { + Tcl_SetObjResult(interp, Tcl_NewStringObj("nothing to redo", -1)); + Tcl_SetErrorCode(interp, "TK", "TEXT", "NO_REDO", NULL); + return TCL_ERROR; + } + canRedo = TkUndoCanRedo(textPtr->sharedTextPtr->undoStack); + if (!canUndo || !canRedo) { + GenerateUndoStackEvent(textPtr); + } + break; + case EDIT_RESET: + if (objc != 3) { + Tcl_WrongNumArgs(interp, 3, objv, NULL); + return TCL_ERROR; + } + canUndo = TkUndoCanUndo(textPtr->sharedTextPtr->undoStack); + canRedo = TkUndoCanRedo(textPtr->sharedTextPtr->undoStack); + TkUndoClearStacks(textPtr->sharedTextPtr->undoStack); + if (canUndo || canRedo) { + GenerateUndoStackEvent(textPtr); + } + break; + case EDIT_SEPARATOR: + if (objc != 3) { + Tcl_WrongNumArgs(interp, 3, objv, NULL); + return TCL_ERROR; + } + TkUndoInsertUndoSeparator(textPtr->sharedTextPtr->undoStack); + break; + case EDIT_UNDO: + if (objc != 3) { + Tcl_WrongNumArgs(interp, 3, objv, NULL); + return TCL_ERROR; + } + canRedo = TkUndoCanRedo(textPtr->sharedTextPtr->undoStack); + if (TextEditUndo(textPtr)) { + Tcl_SetObjResult(interp, Tcl_NewStringObj("nothing to undo", -1)); + Tcl_SetErrorCode(interp, "TK", "TEXT", "NO_UNDO", NULL); + return TCL_ERROR; + } + canUndo = TkUndoCanUndo(textPtr->sharedTextPtr->undoStack); + if (!canRedo || !canUndo) { + GenerateUndoStackEvent(textPtr); + } + break; + } + return TCL_OK; +} + +/* + *---------------------------------------------------------------------- + * + * TextGetText -- + * + * Returns the text from indexPtr1 to indexPtr2, placing that text in a + * string object which is returned with a refCount of zero. + * + * Since the amount of text may potentially be several megabytes (e.g. + * in text editors built on the text widget), efficiency is very + * important. We may want to investigate the efficiency of the + * Tcl_AppendToObj more carefully (e.g. if we know we are going to be + * appending several thousand lines, we could attempt to pre-allocate a + * larger space). + * + * Also the result is built up as a utf-8 string, but, if we knew we + * wanted it as Unicode, we could potentially save a huge conversion by + * building it up as Unicode directly. This could be as simple as + * replacing Tcl_NewObj by Tcl_NewUnicodeObj. + * + * Results: + * Tcl_Obj of string type containing the specified text. If the + * visibleOnly flag is set to 1, then only those characters which are not + * elided will be returned. Otherwise (flag is 0) all characters in the + * given range are returned. + * + * Side effects: + * Memory will be allocated for the new object. Remember to free it if it + * isn't going to be stored appropriately. + * + *---------------------------------------------------------------------- + */ + +static Tcl_Obj * +TextGetText( + const TkText *textPtr, /* Information about text widget. */ + const TkTextIndex *indexPtr1, + /* Get text from this index... */ + const TkTextIndex *indexPtr2, + /* ...to this index. */ + int visibleOnly) /* If non-zero, then only return non-elided + * characters. */ +{ + TkTextIndex tmpIndex; + Tcl_Obj *resultPtr = Tcl_NewObj(); + + TkTextMakeByteIndex(indexPtr1->tree, textPtr, + TkBTreeLinesTo(textPtr, indexPtr1->linePtr), + indexPtr1->byteIndex, &tmpIndex); + + if (TkTextIndexCmp(indexPtr1, indexPtr2) < 0) { + while (1) { + int offset; + TkTextSegment *segPtr = TkTextIndexToSeg(&tmpIndex, &offset); + int last = segPtr->size, last2; + + if (tmpIndex.linePtr == indexPtr2->linePtr) { + /* + * The last line that was requested must be handled carefully, + * because we may need to break out of this loop in the middle + * of the line. + */ + + if (indexPtr2->byteIndex == tmpIndex.byteIndex) { + break; + } + last2 = indexPtr2->byteIndex - tmpIndex.byteIndex + offset; + if (last2 < last) { + last = last2; + } + } + if (segPtr->typePtr == &tkTextCharType && + !(visibleOnly && TkTextIsElided(textPtr,&tmpIndex,NULL))){ + Tcl_AppendToObj(resultPtr, segPtr->body.chars + offset, + last - offset); + } + TkTextIndexForwBytes(textPtr, &tmpIndex, last-offset, &tmpIndex); + } + } + return resultPtr; +} + +/* + *---------------------------------------------------------------------- + * + * GenerateModifiedEvent -- + * + * Send an event that the text was modified. This is equivalent to: + * event generate $textWidget <<Modified>> + * for all peers of $textWidget. + * + * Results: + * None + * + * Side effects: + * May force the text window into existence. + * + *---------------------------------------------------------------------- + */ + +static void +GenerateModifiedEvent( + TkText *textPtr) /* Information about text widget. */ +{ + for (textPtr = textPtr->sharedTextPtr->peers; textPtr != NULL; + textPtr = textPtr->next) { + Tk_MakeWindowExist(textPtr->tkwin); + TkSendVirtualEvent(textPtr->tkwin, "Modified", NULL); + } +} + +/* + *---------------------------------------------------------------------- + * + * GenerateUndoStackEvent -- + * + * Send an event that the undo or redo stack became empty or unempty. + * This is equivalent to: + * event generate $textWidget <<UndoStack>> + * for all peers of $textWidget. + * + * Results: + * None + * + * Side effects: + * May force the text window (and all peers) into existence. + * + *---------------------------------------------------------------------- + */ + +static void +GenerateUndoStackEvent( + TkText *textPtr) /* Information about text widget. */ +{ + for (textPtr = textPtr->sharedTextPtr->peers; textPtr != NULL; + textPtr = textPtr->next) { + Tk_MakeWindowExist(textPtr->tkwin); + TkSendVirtualEvent(textPtr->tkwin, "UndoStack", NULL); + } +} + +/* + *---------------------------------------------------------------------- + * + * UpdateDirtyFlag -- + * + * Updates the dirtyness of the text widget + * + * Results: + * None + * + * Side effects: + * None. + * + *---------------------------------------------------------------------- + */ + +static void +UpdateDirtyFlag( + TkSharedText *sharedTextPtr)/* Information about text widget. */ +{ + int oldDirtyFlag; + + /* + * If we've been forced to be dirty, we stay dirty (until explicitly + * reset, of course). + */ + + if (sharedTextPtr->dirtyMode == TK_TEXT_DIRTY_FIXED) { + return; + } + + if (sharedTextPtr->isDirty < 0 + && sharedTextPtr->dirtyMode == TK_TEXT_DIRTY_NORMAL) { + /* + * If dirty flag is negative, only redo operations can make it zero + * again. If we do a normal operation, it can never become zero any + * more (other than by explicit reset). + */ + + sharedTextPtr->dirtyMode = TK_TEXT_DIRTY_FIXED; + return; + } + + oldDirtyFlag = sharedTextPtr->isDirty; + if (sharedTextPtr->dirtyMode == TK_TEXT_DIRTY_UNDO) { + sharedTextPtr->isDirty--; + } else { + sharedTextPtr->isDirty++; + } + + if (sharedTextPtr->isDirty == 0 || oldDirtyFlag == 0) { + GenerateModifiedEvent(sharedTextPtr->peers); + } +} + +/* + *---------------------------------------------------------------------- + * + * RunAfterSyncCmd -- + * + * This function is called by the event loop and executes the command + * scheduled by [.text sync -command $cmd]. + * + * Results: + * None. + * + * Side effects: + * Anything may happen, depending on $cmd contents. + * + *---------------------------------------------------------------------- + */ + +static void +RunAfterSyncCmd( + ClientData clientData) /* Information about text widget. */ +{ + register TkText *textPtr = (TkText *) clientData; + int code; + + if ((textPtr->tkwin == NULL) || (textPtr->flags & DESTROYED)) { + /* + * The widget has been deleted. Don't do anything. + */ + + if (--textPtr->refCount == 0) { + ckfree((char *) textPtr); + } + return; + } + + Tcl_Preserve((ClientData) textPtr->interp); + code = Tcl_EvalObjEx(textPtr->interp, textPtr->afterSyncCmd, TCL_EVAL_GLOBAL); + if (code == TCL_ERROR) { + Tcl_AddErrorInfo(textPtr->interp, "\n (text sync)"); + Tcl_BackgroundError(textPtr->interp); + } + Tcl_Release((ClientData) textPtr->interp); + Tcl_DecrRefCount(textPtr->afterSyncCmd); + textPtr->afterSyncCmd = NULL; +} + +/* + *---------------------------------------------------------------------- + * + * SearchPerform -- + * + * Overall control of search process. Is given a pattern, a starting + * index and an ending index, and attempts to perform a search. This + * function is actually completely independent of Tk, and could in the + * future be split off. + * + * Results: + * Standard Tcl result code. In particular, if fromPtr or toPtr are not + * considered valid by the 'lineIndexProc', an error will be thrown and + * no search performed. + * + * Side effects: + * See 'SearchCore'. + * + *---------------------------------------------------------------------- + */ + +static int +SearchPerform( + Tcl_Interp *interp, /* For error messages. */ + SearchSpec *searchSpecPtr, /* Search parameters. */ + Tcl_Obj *patObj, /* Contains an exact string or a regexp + * pattern. Must have a refCount > 0. */ + Tcl_Obj *fromPtr, /* Contains information describing the first + * index. */ + Tcl_Obj *toPtr) /* NULL or information describing the last + * index. */ +{ + /* + * Find the starting line and starting offset (measured in Unicode chars + * for regexp search, utf-8 bytes for exact search). + */ + + if (searchSpecPtr->lineIndexProc(interp, fromPtr, searchSpecPtr, + &searchSpecPtr->startLine, + &searchSpecPtr->startOffset) != TCL_OK) { + return TCL_ERROR; + } + + /* + * Find the optional end location, similarly. + */ + + if (toPtr != NULL) { + const TkTextIndex *indexToPtr, *indexFromPtr; + TkText *textPtr = searchSpecPtr->clientData; + + indexToPtr = TkTextGetIndexFromObj(interp, textPtr, toPtr); + if (indexToPtr == NULL) { + return TCL_ERROR; + } + indexFromPtr = TkTextGetIndexFromObj(interp, textPtr, fromPtr); + + /* + * Check for any empty search range here. It might be better in the + * future to embed that in SearchCore (whose default behaviour is to + * wrap when given a negative search range). + */ + + if (TkTextIndexCmp(indexFromPtr, indexToPtr) == + (searchSpecPtr->backwards ? -1 : 1)) { + return TCL_OK; + } + + if (searchSpecPtr->lineIndexProc(interp, toPtr, searchSpecPtr, + &searchSpecPtr->stopLine, + &searchSpecPtr->stopOffset) != TCL_OK) { + return TCL_ERROR; + } + } else { + searchSpecPtr->stopLine = -1; + } + + /* + * Scan through all of the lines of the text circularly, starting at the + * given index. 'patObj' is the pattern which may be an exact string or a + * regexp pattern depending on the flags in searchSpecPtr. + */ + + return SearchCore(interp, searchSpecPtr, patObj); +} + +/* + *---------------------------------------------------------------------- + * + * SearchCore -- + * + * The core of the search function. This function is actually completely + * independent of Tk, and could in the future be split off. + * + * The function assumes regexp-based searches operate on Unicode strings, + * and exact searches on utf-8 strings. Therefore the 'foundMatchProc' + * and 'addLineProc' need to be aware of this distinction. + * + * Results: + * Standard Tcl result code. + * + * Side effects: + * Only those of the 'searchSpecPtr->foundMatchProc' which is called + * whenever a match is found. + * + * Note that the way matching across multiple lines is implemented, we + * start afresh with each line we have available, even though we may + * already have examined the contents of that line (and further ones) if + * we were attempting a multi-line match using the previous line. This + * means there may be ways to speed this up a lot by not throwing away + * all the multi-line information one has accumulated. Profiling should + * be done to see where the bottlenecks lie before attempting this, + * however. We would also need to be very careful such optimisation keep + * within the specified search bounds. + * + *---------------------------------------------------------------------- + */ + +static int +SearchCore( + Tcl_Interp *interp, /* For error messages. */ + SearchSpec *searchSpecPtr, /* Search parameters. */ + Tcl_Obj *patObj) /* Contains an exact string or a regexp + * pattern. Must have a refCount > 0. */ +{ + /* + * For exact searches these are utf-8 char* offsets, for regexp searches + * they are Unicode char offsets. + */ + + int firstOffset, lastOffset, matchOffset, matchLength; + int passes; + int lineNum = searchSpecPtr->startLine; + int code = TCL_OK; + Tcl_Obj *theLine; + int alreadySearchOffset = -1; + + const char *pattern = NULL; /* For exact searches only. */ + int firstNewLine = -1; /* For exact searches only. */ + Tcl_RegExp regexp = NULL; /* For regexp searches only. */ + + /* + * These items are for backward regexp searches only. They are for two + * purposes: to allow us to report backwards matches in the correct order, + * even though the implementation uses repeated forward searches; and to + * provide for overlap checking between backwards matches on different + * text lines. + */ + +#define LOTS_OF_MATCHES 20 + int matchNum = LOTS_OF_MATCHES; + int smArray[2 * LOTS_OF_MATCHES]; + int *storeMatch = smArray; + int *storeLength = smArray + LOTS_OF_MATCHES; + int lastBackwardsLineMatch = -1; + int lastBackwardsMatchOffset = -1; + + if (searchSpecPtr->exact) { + /* + * Convert the pattern to lower-case if we're supposed to ignore case. + */ + + if (searchSpecPtr->noCase) { + patObj = Tcl_DuplicateObj(patObj); + + /* + * This can change the length of the string behind the object's + * back, so ensure it is correctly synchronised. + */ + + Tcl_SetObjLength(patObj, Tcl_UtfToLower(Tcl_GetString(patObj))); + } + } else { + /* + * Compile the regular expression. We want '^$' to match after and + * before \n respectively, so use the TCL_REG_NLANCH flag. + */ + + regexp = Tcl_GetRegExpFromObj(interp, patObj, + (searchSpecPtr->noCase ? TCL_REG_NOCASE : 0) + | (searchSpecPtr->noLineStop ? 0 : TCL_REG_NLSTOP) + | TCL_REG_ADVANCED | TCL_REG_CANMATCH | TCL_REG_NLANCH); + if (regexp == NULL) { + return TCL_ERROR; + } + } + + /* + * For exact strings, we want to know where the first newline is, and we + * will also use this as a flag to test whether it is even possible to + * match the pattern on a single line. If not we will have to search + * across multiple lines. + */ + + if (searchSpecPtr->exact) { + const char *nl; + + /* + * We only need to set the matchLength once for exact searches, and we + * do it here. It is also used below as the actual pattern length, so + * it has dual purpose. + */ + + pattern = Tcl_GetString(patObj); + matchLength = patObj->length; + nl = strchr(pattern, '\n'); + + /* + * If there is no newline, or it is the very end of the string, then + * we don't need any special treatment, since single-line matching + * will work fine. + */ + + if (nl != NULL && nl[1] != '\0') { + firstNewLine = (nl - pattern); + } + } else { + matchLength = 0; /* Only needed to prevent compiler warnings. */ + } + + /* + * Keep a reference here, so that we can be sure the object doesn't + * disappear behind our backs and invalidate its contents which we are + * using. + */ + + Tcl_IncrRefCount(patObj); + + /* + * For building up the current line being checked. + */ + + theLine = Tcl_NewObj(); + Tcl_IncrRefCount(theLine); + + for (passes = 0; passes < 2; ) { + ClientData lineInfo; + int linesSearched = 1; + int extraLinesSearched = 0; + + if (lineNum >= searchSpecPtr->numLines) { + /* + * Don't search the dummy last line of the text. + */ + + goto nextLine; + } + + /* + * Extract the text from the line, storing its length in 'lastOffset' + * (in bytes if exact, chars if regexp), since obviously the length is + * the maximum offset at which it is possible to find something on + * this line, which is what 'lastOffset' represents. + */ + + lineInfo = searchSpecPtr->addLineProc(lineNum, searchSpecPtr, theLine, + &lastOffset, &linesSearched); + + if (lineInfo == NULL) { + /* + * This should not happen, since 'lineNum' should be valid in the + * call above. However, let's try to be flexible and not cause a + * crash below. + */ + + goto nextLine; + } + + if (lineNum == searchSpecPtr->stopLine && searchSpecPtr->backwards) { + firstOffset = searchSpecPtr->stopOffset; + } else { + firstOffset = 0; + } + + if (alreadySearchOffset != -1) { + if (searchSpecPtr->backwards) { + if (alreadySearchOffset < lastOffset) { + lastOffset = alreadySearchOffset; + } + } else { + if (alreadySearchOffset > firstOffset) { + firstOffset = alreadySearchOffset; + } + } + alreadySearchOffset = -1; + } + + if (lineNum == searchSpecPtr->startLine) { + /* + * The starting line is tricky: the first time we see it we check + * one part of the line, and the second pass through we check the + * other part of the line. + */ + + passes++; + if ((passes == 1) ^ searchSpecPtr->backwards) { + /* + * Forward search and first pass, or backward search and + * second pass. + * + * Only use the last part of the line. + */ + + if (searchSpecPtr->startOffset > firstOffset) { + firstOffset = searchSpecPtr->startOffset; + } + if ((firstOffset >= lastOffset) + && ((lastOffset != 0) || searchSpecPtr->exact)) { + goto nextLine; + } + } else { + /* + * Use only the first part of the line. + */ + + if (searchSpecPtr->startOffset < lastOffset) { + lastOffset = searchSpecPtr->startOffset; + } + } + } + + /* + * Check for matches within the current line 'lineNum'. If so, and if + * we're searching backwards or for all matches, repeat the search + * until we find the last match in the line. The 'lastOffset' is one + * beyond the last position in the line at which a match is allowed to + * begin. + */ + + matchOffset = -1; + + if (searchSpecPtr->exact) { + int maxExtraLines = 0; + const char *startOfLine = Tcl_GetString(theLine); + + CLANG_ASSERT(pattern); + do { + Tcl_UniChar ch; + const char *p; + int lastFullLine = lastOffset; + + if (firstNewLine == -1) { + if (searchSpecPtr->strictLimits + && (firstOffset + matchLength > lastOffset)) { + /* + * Not enough characters to match. + */ + + break; + } + + /* + * Single line matching. We want to scan forwards or + * backwards as appropriate. + */ + + if (searchSpecPtr->backwards) { + /* + * Search back either from the previous match or from + * 'startOfLine + lastOffset - 1' until we find a + * match. + */ + + const char c = pattern[0]; + + if (alreadySearchOffset != -1) { + p = startOfLine + alreadySearchOffset; + alreadySearchOffset = -1; + } else { + p = startOfLine + lastOffset -1; + } + while (p >= startOfLine + firstOffset) { + if (p[0] == c && !strncmp(p, pattern, + (unsigned) matchLength)) { + goto backwardsMatch; + } + p--; + } + break; + } else { + p = strstr(startOfLine + firstOffset, pattern); + } + if (p == NULL) { + /* + * Single line match failed. + */ + + break; + } + } else if (firstNewLine >= (lastOffset - firstOffset)) { + /* + * Multi-line match, but not enough characters to match. + */ + + break; + } else { + /* + * Multi-line match has only one possible match position, + * because we know where the '\n' is. + */ + + p = startOfLine + lastOffset - firstNewLine - 1; + if (strncmp(p, pattern, (unsigned) firstNewLine + 1)) { + /* + * No match. + */ + + break; + } else { + int extraLines = 1; + + /* + * If we find a match that overlaps more than one + * line, we will use this value to determine the first + * allowed starting offset for the following search + * (to avoid overlapping results). + */ + + int lastTotal = lastOffset; + int skipFirst = lastOffset - firstNewLine -1; + + /* + * We may be able to match if given more text. The + * following 'while' block handles multi-line exact + * searches. + */ + + while (1) { + lastFullLine = lastTotal; + + if (lineNum+extraLines>=searchSpecPtr->numLines) { + p = NULL; + break; + } + + /* + * Only add the line if we haven't already done so + * already. + */ + + if (extraLines > maxExtraLines) { + if (searchSpecPtr->addLineProc(lineNum + + extraLines, searchSpecPtr, theLine, + &lastTotal, &extraLines) == NULL) { + p = NULL; + if (!searchSpecPtr->backwards) { + extraLinesSearched = extraLines; + } + break; + } + maxExtraLines = extraLines; + } + + startOfLine = Tcl_GetString(theLine); + p = startOfLine + skipFirst; + + /* + * Use the fact that 'matchLength = patLength' for + * exact searches. + */ + + if ((lastTotal - skipFirst) >= matchLength) { + /* + * We now have enough text to match, so we + * make a final test and break whatever the + * result. + */ + + if (strncmp(p,pattern,(unsigned)matchLength)) { + p = NULL; + } + break; + } else { + /* + * Not enough text yet, but check the prefix. + */ + + if (strncmp(p, pattern, + (unsigned)(lastTotal - skipFirst))) { + p = NULL; + break; + } + + /* + * The prefix matches, so keep looking. + */ + } + extraLines++; + } + /* + * If we reach here, with p != NULL, we've found a + * multi-line match, else we started a multi-match but + * didn't finish it off, so we go to the next line. + */ + + if (p == NULL) { + break; + } + + /* + * We've found a multi-line match. + */ + + if (extraLines > 0) { + extraLinesSearched = extraLines - 1; + } + } + } + + backwardsMatch: + if ((p - startOfLine) >= lastOffset) { + break; + } + + /* + * Remember the match. + */ + + matchOffset = p - startOfLine; + + if (searchSpecPtr->all && + !searchSpecPtr->foundMatchProc(lineNum, searchSpecPtr, + lineInfo, theLine, matchOffset, matchLength)) { + /* + * We reached the end of the search. + */ + + goto searchDone; + } + + if (!searchSpecPtr->overlap) { + if (searchSpecPtr->backwards) { + alreadySearchOffset = p - startOfLine; + if (firstNewLine != -1) { + break; + } else { + alreadySearchOffset -= matchLength; + } + } else { + firstOffset = p - startOfLine + matchLength; + if (firstOffset >= lastOffset) { + /* + * Now, we have to be careful not to find + * overlapping matches either on the same or + * following lines. Assume that if we did find + * something, it goes until the last extra line we + * added. + * + * We can break out of the loop, since we know no + * more will be found. + */ + + if (!searchSpecPtr->backwards) { + alreadySearchOffset = + firstOffset - lastFullLine; + break; + } + } + } + } else { + if (searchSpecPtr->backwards) { + alreadySearchOffset = p - startOfLine - 1; + if (alreadySearchOffset < 0) { + break; + } + } else { + firstOffset = p - startOfLine + + Tcl_UtfToUniChar(startOfLine+matchOffset,&ch); + } + } + } while (searchSpecPtr->all); + } else { + int maxExtraLines = 0; + int matches = 0; + int lastNonOverlap = -1; + + do { + Tcl_RegExpInfo info; + int match; + int lastFullLine = lastOffset; + + match = Tcl_RegExpExecObj(interp, regexp, theLine, + firstOffset, 1, (firstOffset>0 ? TCL_REG_NOTBOL : 0)); + if (match < 0) { + code = TCL_ERROR; + goto searchDone; + } + Tcl_RegExpGetInfo(regexp, &info); + + /* + * If we don't have a match, or if we do, but it extends to + * the end of the line, we must try to add more lines to get a + * full greedy match. + */ + + if (!match || + ((info.extendStart == info.matches[0].start) + && (info.matches[0].end == lastOffset-firstOffset))) { + int extraLines = 0; + int prevFullLine; + + /* + * If we find a match that overlaps more than one line, we + * will use this value to determine the first allowed + * starting offset for the following search (to avoid + * overlapping results). + */ + + int lastTotal = lastOffset; + + if ((lastBackwardsLineMatch != -1) + && (lastBackwardsLineMatch == (lineNum + 1))) { + lastNonOverlap = lastTotal; + } + + if (info.extendStart < 0) { + /* + * No multi-line match is possible. + */ + + break; + } + + /* + * We may be able to match if given more text. The + * following 'while' block handles multi-line regexp + * searches. + */ + + while (1) { + prevFullLine = lastTotal; + + /* + * Move firstOffset to first possible start. + */ + + if (!match) { + firstOffset += info.extendStart; + } + if (firstOffset >= lastOffset) { + /* + * We're being told that the only possible new + * match is starting after the end of the line. + * But, that is the next line which we will handle + * when we look at that line. + */ + + if (!match && !searchSpecPtr->backwards + && (firstOffset == 0)) { + extraLinesSearched = extraLines; + } + break; + } + + if (lineNum + extraLines >= searchSpecPtr->numLines) { + break; + } + + /* + * Add next line, provided we haven't already done so. + */ + + if (extraLines > maxExtraLines) { + if (searchSpecPtr->addLineProc(lineNum + + extraLines, searchSpecPtr, theLine, + &lastTotal, &extraLines) == NULL) { + /* + * There are no more acceptable lines, so we + * can say we have searched all of these. + */ + + if (!match && !searchSpecPtr->backwards) { + extraLinesSearched = extraLines; + } + break; + } + + maxExtraLines = extraLines; + if ((lastBackwardsLineMatch != -1) + && (lastBackwardsLineMatch + == (lineNum + extraLines + 1))) { + lastNonOverlap = lastTotal; + } + } + + match = Tcl_RegExpExecObj(interp, regexp, theLine, + firstOffset, 1, + ((firstOffset > 0) ? TCL_REG_NOTBOL : 0)); + if (match < 0) { + code = TCL_ERROR; + goto searchDone; + } + Tcl_RegExpGetInfo(regexp, &info); + + /* + * Unfortunately there are bugs in Tcl's regexp + * library, which tells us that info.extendStart is + * zero when it should not be (should be -1), which + * makes our task a bit more complicated here. We + * check if there was a match, and the end of the + * match leaves an entire extra line unmatched, then + * we stop searching. Clearly it still might sometimes + * be possible to add more text and match again, but + * Tcl's regexp library doesn't tell us that. + * + * This means we often add and search one more line + * than might be necessary if Tcl were able to give us + * a correct value of info.extendStart under all + * circumstances. + */ + + if ((match && + firstOffset+info.matches[0].end != lastTotal && + firstOffset+info.matches[0].end < prevFullLine) + || info.extendStart < 0) { + break; + } + + /* + * If there is a match, but that match starts after + * the end of the first line, then we'll handle that + * next time around, when we're actually looking at + * that line. + */ + + if (match && (info.matches[0].start >= lastOffset)) { + break; + } + if (match && ((firstOffset + info.matches[0].end) + >= prevFullLine)) { + if (extraLines > 0) { + extraLinesSearched = extraLines - 1; + } + lastFullLine = prevFullLine; + } + + /* + * The prefix matches, so keep looking. + */ + + extraLines++; + } + + /* + * If we reach here with 'match == 1', we've found a + * multi-line match, which we will record in the code + * which follows directly else we started a multi-line + * match but didn't finish it off, so we go to the next + * line. + */ + + if (!match) { + /* + * Here is where we could perform an optimisation, + * since we have already retrieved the contents of the + * next line (perhaps many more), so we shouldn't + * really throw it all away and start again. This + * could be particularly important for complex regexp + * searches. + * + * This 'break' will take us to just before the + * 'nextLine:' below. + */ + + break; + } + + if (lastBackwardsLineMatch != -1) { + if ((lineNum + linesSearched + extraLinesSearched) + == lastBackwardsLineMatch) { + /* + * Possible overlap or inclusion. + */ + + int thisOffset = firstOffset + info.matches[0].end + - info.matches[0].start; + + if (lastNonOverlap != -1) { + /* + * Possible overlap or enclosure. + */ + + if (thisOffset-lastNonOverlap >= + lastBackwardsMatchOffset+matchLength){ + /* + * Totally encloses previous match, so + * forget the previous match. + */ + + lastBackwardsLineMatch = -1; + } else if ((thisOffset - lastNonOverlap) + > lastBackwardsMatchOffset) { + /* + * Overlap. Previous match is ok, and the + * current match is only ok if we are + * searching with -overlap. + */ + + if (searchSpecPtr->overlap) { + goto recordBackwardsMatch; + } else { + match = 0; + break; + } + } else { + /* + * No overlap, although the same line was + * reached. + */ + + goto recordBackwardsMatch; + } + } else { + /* + * No overlap. + */ + + goto recordBackwardsMatch; + } + } else if (lineNum+linesSearched+extraLinesSearched + < lastBackwardsLineMatch) { + /* + * No overlap. + */ + + goto recordBackwardsMatch; + } else { + /* + * Totally enclosed. + */ + + lastBackwardsLineMatch = -1; + } + } + + } else { + /* + * Matched in a single line. + */ + + if (lastBackwardsLineMatch != -1) { + recordBackwardsMatch: + searchSpecPtr->foundMatchProc(lastBackwardsLineMatch, + searchSpecPtr, NULL, NULL, + lastBackwardsMatchOffset, matchLength); + lastBackwardsLineMatch = -1; + if (!searchSpecPtr->all) { + goto searchDone; + } + } + } + + firstOffset += info.matches[0].start; + if (firstOffset >= lastOffset) { + break; + } + + /* + * Update our local variables with the match, if we haven't + * yet found anything, or if we're doing '-all' or + * '-backwards' _and_ this match isn't fully enclosed in the + * previous match. + */ + + if (matchOffset == -1 || + ((searchSpecPtr->all || searchSpecPtr->backwards) + && ((firstOffset < matchOffset) + || ((firstOffset + info.matches[0].end + - info.matches[0].start) + > (matchOffset + matchLength))))) { + + matchOffset = firstOffset; + matchLength = info.matches[0].end - info.matches[0].start; + + if (searchSpecPtr->backwards) { + /* + * To get backwards searches in the correct order, we + * must store them away here. + */ + + if (matches == matchNum) { + /* + * We've run out of space in our normal store, so + * we must allocate space for these backwards + * matches on the heap. + */ + + int *newArray = + ckalloc(4 * matchNum * sizeof(int)); + memcpy(newArray, storeMatch, matchNum*sizeof(int)); + memcpy(newArray + 2*matchNum, storeLength, + matchNum * sizeof(int)); + if (storeMatch != smArray) { + ckfree(storeMatch); + } + matchNum *= 2; + storeMatch = newArray; + storeLength = newArray + matchNum; + } + storeMatch[matches] = matchOffset; + storeLength[matches] = matchLength; + matches++; + } else { + /* + * Now actually record the match, but only if we are + * doing an '-all' search. + */ + + if (searchSpecPtr->all && + !searchSpecPtr->foundMatchProc(lineNum, + searchSpecPtr, lineInfo, theLine, matchOffset, + matchLength)) { + /* + * We reached the end of the search. + */ + + goto searchDone; + } + } + + /* + * For forward matches, unless we allow overlaps, we move + * this on by the length of the current match so that we + * explicitly disallow overlapping matches. + */ + + if (matchLength > 0 && !searchSpecPtr->overlap + && !searchSpecPtr->backwards) { + firstOffset += matchLength; + if (firstOffset >= lastOffset) { + /* + * Now, we have to be careful not to find + * overlapping matches either on the same or + * following lines. Assume that if we did find + * something, it goes until the last extra line we + * added. + * + * We can break out of the loop, since we know no + * more will be found. + */ + + alreadySearchOffset = firstOffset - lastFullLine; + break; + } + + /* + * We'll add this on again just below. + */ + + firstOffset --; + } + } + + /* + * Move the starting point on, in case we are doing repeated + * or backwards searches (for the latter, we actually do + * repeated forward searches). + */ + + firstOffset++; + } while (searchSpecPtr->backwards || searchSpecPtr->all); + + if (matches > 0) { + /* + * Now we have all the matches in our array, but not stored + * with 'foundMatchProc' yet. + */ + + matches--; + matchOffset = storeMatch[matches]; + matchLength = storeLength[matches]; + while (--matches >= 0) { + if (lineNum == searchSpecPtr->stopLine) { + /* + * It appears as if a condition like: + * + * if (storeMatch[matches]<searchSpecPtr->stopOffset) + * break; + * + * might be needed here, but no test case has been + * found which would exercise such a problem. + */ + } + if (storeMatch[matches] + storeLength[matches] + >= matchOffset + matchLength) { + /* + * The new match totally encloses the previous one, so + * we overwrite the previous one. + */ + + matchOffset = storeMatch[matches]; + matchLength = storeLength[matches]; + continue; + } + if (!searchSpecPtr->overlap) { + if (storeMatch[matches] + storeLength[matches] + > matchOffset) { + continue; + } + } + searchSpecPtr->foundMatchProc(lineNum, searchSpecPtr, + lineInfo, theLine, matchOffset, matchLength); + if (!searchSpecPtr->all) { + goto searchDone; + } + matchOffset = storeMatch[matches]; + matchLength = storeLength[matches]; + } + if (searchSpecPtr->all && matches > 0) { + /* + * We only need to do this for the '-all' case, because + * just below we will call the foundMatchProc for the + * non-all case. + */ + + searchSpecPtr->foundMatchProc(lineNum, searchSpecPtr, + lineInfo, theLine, matchOffset, matchLength); + } else { + lastBackwardsLineMatch = lineNum; + lastBackwardsMatchOffset = matchOffset; + } + } + } + + /* + * If the 'all' flag is set, we will already have stored all matches, + * so we just proceed to the next line. + * + * If not, and there is a match we need to store that information and + * we are done. + */ + + if ((lastBackwardsLineMatch == -1) && (matchOffset >= 0) + && !searchSpecPtr->all) { + searchSpecPtr->foundMatchProc(lineNum, searchSpecPtr, lineInfo, + theLine, matchOffset, matchLength); + goto searchDone; + } + + /* + * Go to the next (or previous) line; + */ + + nextLine: + linesSearched += extraLinesSearched; + + while (linesSearched-- > 0) { + /* + * If we have just completed the 'stopLine', we are done. + */ + + if (lineNum == searchSpecPtr->stopLine) { + goto searchDone; + } + + if (searchSpecPtr->backwards) { + lineNum--; + + if (lastBackwardsLineMatch != -1 + && ((lineNum < 0) + || (lineNum + 2 < lastBackwardsLineMatch))) { + searchSpecPtr->foundMatchProc(lastBackwardsLineMatch, + searchSpecPtr, NULL, NULL, + lastBackwardsMatchOffset, matchLength); + lastBackwardsLineMatch = -1; + if (!searchSpecPtr->all) { + goto searchDone; + } + } + + if (lineNum < 0) { + lineNum = searchSpecPtr->numLines-1; + } + if (!searchSpecPtr->exact) { + /* + * The 'exact' search loops above are designed to give us + * an accurate picture of the number of lines which we can + * skip here. For 'regexp' searches, on the other hand, + * which can match potentially variable lengths, we cannot + * skip multiple lines when searching backwards. Therefore + * we only allow one line to be skipped here. + */ + + break; + } + } else { + lineNum++; + if (lineNum >= searchSpecPtr->numLines) { + lineNum = 0; + } + } + if (lineNum == searchSpecPtr->startLine && linesSearched > 0) { + /* + * We've just searched all the way round and have gone right + * through the start line without finding anything in the last + * attempt. + */ + + break; + } + } + + Tcl_SetObjLength(theLine, 0); + } + searchDone: + + if (lastBackwardsLineMatch != -1) { + searchSpecPtr->foundMatchProc(lastBackwardsLineMatch, searchSpecPtr, + NULL, NULL, lastBackwardsMatchOffset, matchLength); + } + + /* + * Free up the cached line and pattern. + */ + + Tcl_DecrRefCount(theLine); + Tcl_DecrRefCount(patObj); + + /* + * Free up any extra space we allocated. + */ + + if (storeMatch != smArray) { + ckfree(storeMatch); + } + + return code; +} + +/* + *---------------------------------------------------------------------- + * + * GetLineStartEnd - + * + * Converts an internal TkTextLine ptr into a Tcl string obj containing + * the line number. (Handler for the 'line' configuration option type.) + * + * Results: + * Tcl_Obj containing the string representation of the line value. + * + * Side effects: + * Creates a new Tcl_Obj. + * + *---------------------------------------------------------------------- + */ + +static Tcl_Obj * +GetLineStartEnd( + ClientData clientData, + Tk_Window tkwin, + char *recordPtr, /* Pointer to widget record. */ + int internalOffset) /* Offset within *recordPtr containing the + * line value. */ +{ + TkTextLine *linePtr = *(TkTextLine **)(recordPtr + internalOffset); + + if (linePtr == NULL) { + return Tcl_NewObj(); + } + return Tcl_NewIntObj(1 + TkBTreeLinesTo(NULL, linePtr)); +} + +/* + *---------------------------------------------------------------------- + * + * SetLineStartEnd -- + * + * Converts a Tcl_Obj representing a widget's (start or end) line into a + * TkTextLine* value. (Handler for the 'line' configuration option type.) + * + * Results: + * Standard Tcl result. + * + * Side effects: + * May store the TkTextLine* value into the internal representation + * pointer. May change the pointer to the Tcl_Obj to NULL to indicate + * that the specified string was empty and that is acceptable. + * + *---------------------------------------------------------------------- + */ + +static int +SetLineStartEnd( + ClientData clientData, + Tcl_Interp *interp, /* Current interp; may be used for errors. */ + Tk_Window tkwin, /* Window for which option is being set. */ + Tcl_Obj **value, /* Pointer to the pointer to the value object. + * We use a pointer to the pointer because we + * may need to return a value (NULL). */ + char *recordPtr, /* Pointer to storage for the widget record. */ + int internalOffset, /* Offset within *recordPtr at which the + * internal value is to be stored. */ + char *oldInternalPtr, /* Pointer to storage for the old value. */ + int flags) /* Flags for the option, set Tk_SetOptions. */ +{ + TkTextLine *linePtr = NULL; + char *internalPtr; + TkText *textPtr = (TkText *) recordPtr; + + if (internalOffset >= 0) { + internalPtr = recordPtr + internalOffset; + } else { + internalPtr = NULL; + } + + if (flags & TK_OPTION_NULL_OK && ObjectIsEmpty(*value)) { + *value = NULL; + } else { + int line; + + if (Tcl_GetIntFromObj(interp, *value, &line) != TCL_OK) { + return TCL_ERROR; + } + linePtr = TkBTreeFindLine(textPtr->sharedTextPtr->tree, NULL, line-1); + } + + if (internalPtr != NULL) { + *((TkTextLine **) oldInternalPtr) = *((TkTextLine **) internalPtr); + *((TkTextLine **) internalPtr) = linePtr; + } + return TCL_OK; +} + +/* + *---------------------------------------------------------------------- + * + * RestoreLineStartEnd -- + * + * Restore a line option value from a saved value. (Handler for the + * 'line' configuration option type.) + * + * Results: + * None. + * + * Side effects: + * Restores the old value. + * + *---------------------------------------------------------------------- + */ + +static void +RestoreLineStartEnd( + ClientData clientData, + Tk_Window tkwin, + char *internalPtr, /* Pointer to storage for value. */ + char *oldInternalPtr) /* Pointer to old value. */ +{ + *(TkTextLine **)internalPtr = *(TkTextLine **)oldInternalPtr; +} + +/* + *---------------------------------------------------------------------- + * + * ObjectIsEmpty -- + * + * This function tests whether the string value of an object is empty. + * + * Results: + * The return value is 1 if the string value of objPtr has length zero, + * and 0 otherwise. + * + * Side effects: + * May cause object shimmering, since this function can force a + * conversion to a string object. + * + *---------------------------------------------------------------------- + */ + +static int +ObjectIsEmpty( + Tcl_Obj *objPtr) /* Object to test. May be NULL. */ +{ + if (objPtr == NULL) { + return 1; + } + if (objPtr->bytes != NULL) { + return (objPtr->length == 0); + } + (void)Tcl_GetString(objPtr); + return (objPtr->length == 0); +} + +/* + *---------------------------------------------------------------------- + * + * TkpTesttextCmd -- + * + * This function implements the "testtext" command. It provides a set of + * functions for testing text widgets and the associated functions in + * tkText*.c. + * + * Results: + * A standard Tcl result. + * + * Side effects: + * Depends on option; see below. + * + *---------------------------------------------------------------------- + */ + +int +TkpTesttextCmd( + ClientData clientData, /* Main window for application. */ + Tcl_Interp *interp, /* Current interpreter. */ + int objc, /* Number of arguments. */ + Tcl_Obj *const objv[]) /* Argument strings. */ +{ + TkText *textPtr; + size_t len; + int lineIndex, byteIndex, byteOffset; + TkTextIndex index; + char buf[64]; + Tcl_CmdInfo info; + + if (objc < 3) { + return TCL_ERROR; + } + + if (Tcl_GetCommandInfo(interp, Tcl_GetString(objv[1]), &info) == 0) { + return TCL_ERROR; + } + textPtr = info.objClientData; + len = strlen(Tcl_GetString(objv[2])); + if (strncmp(Tcl_GetString(objv[2]), "byteindex", len) == 0) { + if (objc != 5) { + return TCL_ERROR; + } + lineIndex = atoi(Tcl_GetString(objv[3])) - 1; + byteIndex = atoi(Tcl_GetString(objv[4])); + + TkTextMakeByteIndex(textPtr->sharedTextPtr->tree, textPtr, lineIndex, + byteIndex, &index); + } else if (strncmp(Tcl_GetString(objv[2]), "forwbytes", len) == 0) { + if (objc != 5) { + return TCL_ERROR; + } + if (TkTextGetIndex(interp, textPtr, Tcl_GetString(objv[3]), &index) != TCL_OK) { + return TCL_ERROR; + } + byteOffset = atoi(Tcl_GetString(objv[4])); + TkTextIndexForwBytes(textPtr, &index, byteOffset, &index); + } else if (strncmp(Tcl_GetString(objv[2]), "backbytes", len) == 0) { + if (objc != 5) { + return TCL_ERROR; + } + if (TkTextGetIndex(interp, textPtr, Tcl_GetString(objv[3]), &index) != TCL_OK) { + return TCL_ERROR; + } + byteOffset = atoi(Tcl_GetString(objv[4])); + TkTextIndexBackBytes(textPtr, &index, byteOffset, &index); + } else { + return TCL_ERROR; + } + + TkTextSetMark(textPtr, "insert", &index); + TkTextPrintIndex(textPtr, &index, buf); + Tcl_SetObjResult(interp, Tcl_ObjPrintf("%s %d", buf, index.byteIndex)); + return TCL_OK; +} + +/* + * Local Variables: + * mode: c + * c-basic-offset: 4 + * fill-column: 78 + * End: + */ |