/*
* 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 "tkInt.h"
#include "tkUndo.h"
#include "default.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,
TkSizeT internalOffset, char *oldInternalPtr,
int flags);
static Tcl_Obj * GetLineStartEnd(ClientData clientData,
Tk_Window tkwin, char *recordPtr,
TkSizeT 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, TCL_INDEX_NONE,
offsetof(TkText, autoSeparators),
TK_OPTION_DONT_SET_DEFAULT, 0, 0},
{TK_OPTION_BORDER, "-background", "background", "Background",
DEF_TEXT_BG_COLOR, TCL_INDEX_NONE, offsetof(TkText, border),
0, DEF_TEXT_BG_MONO, 0},
{TK_OPTION_SYNONYM, "-bd", NULL, NULL,
NULL, 0, TCL_INDEX_NONE, 0, "-borderwidth",
TK_TEXT_LINE_GEOMETRY},
{TK_OPTION_SYNONYM, "-bg", NULL, NULL,
NULL, 0, TCL_INDEX_NONE, 0, "-background", 0},
{TK_OPTION_BOOLEAN, "-blockcursor", "blockCursor",
"BlockCursor", DEF_TEXT_BLOCK_CURSOR, TCL_INDEX_NONE,
offsetof(TkText, insertCursorType), 0, 0, 0},
{TK_OPTION_PIXELS, "-borderwidth", "borderWidth", "BorderWidth",
DEF_TEXT_BORDER_WIDTH, TCL_INDEX_NONE, offsetof(TkText, borderWidth),
0, 0, TK_TEXT_LINE_GEOMETRY},
{TK_OPTION_CURSOR, "-cursor", "cursor", "Cursor",
DEF_TEXT_CURSOR, TCL_INDEX_NONE, offsetof(TkText, cursor),
TK_OPTION_NULL_OK, 0, 0},
{TK_OPTION_CUSTOM, "-endline", NULL, NULL,
NULL, TCL_INDEX_NONE, offsetof(TkText, end), TK_OPTION_NULL_OK,
&lineOption, TK_TEXT_LINE_RANGE},
{TK_OPTION_BOOLEAN, "-exportselection", "exportSelection",
"ExportSelection", DEF_TEXT_EXPORT_SELECTION, TCL_INDEX_NONE,
offsetof(TkText, exportSelection), 0, 0, 0},
{TK_OPTION_SYNONYM, "-fg", "foreground", NULL,
NULL, 0, TCL_INDEX_NONE, 0, "-foreground", 0},
{TK_OPTION_FONT, "-font", "font", "Font",
DEF_TEXT_FONT, TCL_INDEX_NONE, offsetof(TkText, tkfont), 0, 0,
TK_TEXT_LINE_GEOMETRY},
{TK_OPTION_COLOR, "-foreground", "foreground", "Foreground",
DEF_TEXT_FG, TCL_INDEX_NONE, offsetof(TkText, fgColor), 0,
0, 0},
{TK_OPTION_PIXELS, "-height", "height", "Height",
DEF_TEXT_HEIGHT, TCL_INDEX_NONE, offsetof(TkText, height), 0, 0, 0},
{TK_OPTION_COLOR, "-highlightbackground", "highlightBackground",
"HighlightBackground", DEF_TEXT_HIGHLIGHT_BG,
TCL_INDEX_NONE, offsetof(TkText, highlightBgColorPtr),
0, 0, 0},
{TK_OPTION_COLOR, "-highlightcolor", "highlightColor", "HighlightColor",
DEF_TEXT_HIGHLIGHT, TCL_INDEX_NONE, offsetof(TkText, highlightColorPtr),
0, 0, 0},
{TK_OPTION_PIXELS, "-highlightthickness", "highlightThickness",
"HighlightThickness", DEF_TEXT_HIGHLIGHT_WIDTH, TCL_INDEX_NONE,
offsetof(TkText, highlightWidth), 0, 0, TK_TEXT_LINE_GEOMETRY},
{TK_OPTION_BORDER, "-inactiveselectbackground","inactiveSelectBackground",
"Foreground",
DEF_TEXT_INACTIVE_SELECT_COLOR,
TCL_INDEX_NONE, offsetof(TkText, inactiveSelBorder),
TK_OPTION_NULL_OK, DEF_TEXT_SELECT_MONO, 0},
{TK_OPTION_BORDER, "-insertbackground", "insertBackground", "Foreground",
DEF_TEXT_INSERT_BG,
TCL_INDEX_NONE, offsetof(TkText, insertBorder),
0, 0, 0},
{TK_OPTION_PIXELS, "-insertborderwidth", "insertBorderWidth",
"BorderWidth", DEF_TEXT_INSERT_BD_COLOR, TCL_INDEX_NONE,
offsetof(TkText, insertBorderWidth), 0,
(ClientData) DEF_TEXT_INSERT_BD_MONO, 0},
{TK_OPTION_INT, "-insertofftime", "insertOffTime", "OffTime",
DEF_TEXT_INSERT_OFF_TIME, TCL_INDEX_NONE, offsetof(TkText, insertOffTime),
0, 0, 0},
{TK_OPTION_INT, "-insertontime", "insertOnTime", "OnTime",
DEF_TEXT_INSERT_ON_TIME, TCL_INDEX_NONE, offsetof(TkText, insertOnTime),
0, 0, 0},
{TK_OPTION_STRING_TABLE,
"-insertunfocussed", "insertUnfocussed", "InsertUnfocussed",
DEF_TEXT_INSERT_UNFOCUSSED, TCL_INDEX_NONE, offsetof(TkText, insertUnfocussed),
0, insertUnfocussedStrings, 0},
{TK_OPTION_PIXELS, "-insertwidth", "insertWidth", "InsertWidth",
DEF_TEXT_INSERT_WIDTH, TCL_INDEX_NONE, offsetof(TkText, insertWidth),
0, 0, 0},
{TK_OPTION_INT, "-maxundo", "maxUndo", "MaxUndo",
DEF_TEXT_MAX_UNDO, TCL_INDEX_NONE, offsetof(TkText, maxUndo),
TK_OPTION_DONT_SET_DEFAULT, 0, 0},
{TK_OPTION_PIXELS, "-padx", "padX", "Pad",
DEF_TEXT_PADX, TCL_INDEX_NONE, offsetof(TkText, padX), 0, 0,
TK_TEXT_LINE_GEOMETRY},
{TK_OPTION_PIXELS, "-pady", "padY", "Pad",
DEF_TEXT_PADY, TCL_INDEX_NONE, offsetof(TkText, padY), 0, 0, 0},
{TK_OPTION_RELIEF, "-relief", "relief", "Relief",
DEF_TEXT_RELIEF, TCL_INDEX_NONE, offsetof(TkText, relief), 0, 0, 0},
{TK_OPTION_BORDER, "-selectbackground", "selectBackground", "Foreground",
DEF_TEXT_SELECT_COLOR, TCL_INDEX_NONE, offsetof(TkText, selBorder),
0, DEF_TEXT_SELECT_MONO, 0},
{TK_OPTION_PIXELS, "-selectborderwidth", "selectBorderWidth",
"BorderWidth", DEF_TEXT_SELECT_BD_COLOR,
offsetof(TkText, selBorderWidthPtr),
offsetof(TkText, selBorderWidth),
TK_OPTION_NULL_OK, DEF_TEXT_SELECT_BD_MONO, 0},
{TK_OPTION_COLOR, "-selectforeground", "selectForeground", "Background",
DEF_TEXT_SELECT_FG_COLOR, TCL_INDEX_NONE, offsetof(TkText, selFgColorPtr),
TK_OPTION_NULL_OK, DEF_TEXT_SELECT_FG_MONO, 0},
{TK_OPTION_BOOLEAN, "-setgrid", "setGrid", "SetGrid",
DEF_TEXT_SET_GRID, TCL_INDEX_NONE, offsetof(TkText, setGrid), 0, 0, 0},
{TK_OPTION_PIXELS, "-spacing1", "spacing1", "Spacing",
DEF_TEXT_SPACING1, TCL_INDEX_NONE, offsetof(TkText, spacing1),
0, 0 , TK_TEXT_LINE_GEOMETRY },
{TK_OPTION_PIXELS, "-spacing2", "spacing2", "Spacing",
DEF_TEXT_SPACING2, TCL_INDEX_NONE, offsetof(TkText, spacing2),
0, 0 , TK_TEXT_LINE_GEOMETRY },
{TK_OPTION_PIXELS, "-spacing3", "spacing3", "Spacing",
DEF_TEXT_SPACING3, TCL_INDEX_NONE, offsetof(TkText, spacing3),
0, 0 , TK_TEXT_LINE_GEOMETRY },
{TK_OPTION_CUSTOM, "-startline", NULL, NULL,
NULL, TCL_INDEX_NONE, offsetof(TkText, start), TK_OPTION_NULL_OK,
&lineOption, TK_TEXT_LINE_RANGE},
{TK_OPTION_STRING_TABLE, "-state", "state", "State",
DEF_TEXT_STATE, TCL_INDEX_NONE, offsetof(TkText, state),
0, stateStrings, 0},
{TK_OPTION_STRING, "-tabs", "tabs", "Tabs",
DEF_TEXT_TABS, offsetof(TkText, tabOptionPtr), TCL_INDEX_NONE,
TK_OPTION_NULL_OK, 0, TK_TEXT_LINE_GEOMETRY},
{TK_OPTION_STRING_TABLE, "-tabstyle", "tabStyle", "TabStyle",
DEF_TEXT_TABSTYLE, TCL_INDEX_NONE, offsetof(TkText, tabStyle),
0, tabStyleStrings, TK_TEXT_LINE_GEOMETRY},
{TK_OPTION_STRING, "-takefocus", "takeFocus", "TakeFocus",
DEF_TEXT_TAKE_FOCUS, TCL_INDEX_NONE, offsetof(TkText, takeFocus),
TK_OPTION_NULL_OK, 0, 0},
{TK_OPTION_BOOLEAN, "-undo", "undo", "Undo",
DEF_TEXT_UNDO, TCL_INDEX_NONE, offsetof(TkText, undo),
TK_OPTION_DONT_SET_DEFAULT, 0 , 0},
{TK_OPTION_INT, "-width", "width", "Width",
DEF_TEXT_WIDTH, TCL_INDEX_NONE, offsetof(TkText, width), 0, 0,
TK_TEXT_LINE_GEOMETRY},
{TK_OPTION_STRING_TABLE, "-wrap", "wrap", "Wrap",
DEF_TEXT_WRAP, TCL_INDEX_NONE, offsetof(TkText, wrapMode),
0, wrapStrings, TK_TEXT_LINE_GEOMETRY},
{TK_OPTION_STRING, "-xscrollcommand", "xScrollCommand", "ScrollCommand",
DEF_TEXT_XSCROLL_COMMAND, TCL_INDEX_NONE, offsetof(TkText, xScrollCmd),
TK_OPTION_NULL_OK, 0, 0},
{TK_OPTION_STRING, "-yscrollcommand", "yScrollCommand", "ScrollCommand",
DEF_TEXT_YSCROLL_COMMAND, TCL_INDEX_NONE, offsetof(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,
TkSizeT matchOffset, TkSizeT matchLength);
typedef int SearchLineIndexProc(Tcl_Interp *interp,
Tcl_Obj *objPtr, struct SearchSpec *searchSpecPtr,
int *linePosPtr, TkSizeT *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. */
TkSizeT startOffset1; /* Index in first line to start at. */
int stopLine; /* Last line to examine, or -1 when we search
* all available text. */
TkSizeT stopOffset1; /* 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 TkSizeT TextFetchSelection(ClientData clientData, TkSizeT offset,
char *buffer, TkSizeT 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 TextPushUndoAction(TkText *textPtr,
Tcl_Obj *undoString, int insert,
const TkTextIndex *index1Ptr,
const TkTextIndex *index2Ptr);
static TkSizeT TextSearchIndexInLine(const SearchSpec *searchSpecPtr,
TkTextLine *linePtr, TkSizeT 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 = (Tk_Window)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. */
{
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 = (TkText *)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 = (TkSharedText *)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 = 0;
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 = NULL;
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 = (char *)
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, 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. */
{
TkText *textPtr = (TkText *)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_NewWideIntObj(x));
Tcl_ListObjAppendElement(interp, listObj, Tcl_NewWideIntObj(y));
Tcl_ListObjAppendElement(interp, listObj, Tcl_NewWideIntObj(width));
Tcl_ListObjAppendElement(interp, listObj, Tcl_NewWideIntObj(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, 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, 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;
TkSizeT length;
const char *option = TkGetStringFromObj(objv[i], &length);
char c;
if (length < 2 || option[0] != '-') {
goto badOption;
}
c = option[1];
if (c == 'c' && !strncmp("-chars", option, length)) {
value = CountIndices(textPtr, indexFromPtr, indexToPtr,
COUNT_CHARS);
} else if (c == 'd' && (length > 8)
&& !strncmp("-displaychars", option, length)) {
value = CountIndices(textPtr, indexFromPtr, indexToPtr,
COUNT_DISPLAY_CHARS);
} else if (c == 'd' && (length > 8)
&& !strncmp("-displayindices", option,length)) {
value = CountIndices(textPtr, indexFromPtr, indexToPtr,
COUNT_DISPLAY_INDICES);
} else if (c == 'd' && (length > 8)
&& !strncmp("-displaylines", option, 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, length)) {
value = CountIndices(textPtr, indexFromPtr, indexToPtr,
COUNT_INDICES);
} else if (c == 'l'
&& !strncmp("-lines", option, length)) {
value = TkBTreeLinesTo(textPtr, indexToPtr->linePtr)
- TkBTreeLinesTo(textPtr, indexFromPtr->linePtr);
} else if (c == 'u'
&& !strncmp("-update", option, length)) {
update = 1;
continue;
} else if (c == 'x'
&& !strncmp("-xpixels", option, 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, 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_NewWideIntObj(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_NewWideIntObj(value));
}
}
if (found == 0) {
/*
* Use the default '-indices'.
*/
int value = CountIndices(textPtr, indexFromPtr, indexToPtr,
COUNT_INDICES);
Tcl_SetObjResult(interp, Tcl_NewWideIntObj(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 = (TkTextIndex *)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 = (char *)ckalloc(objc);
memset(useIdx, 0, objc);
/*
* Do a decreasing order sort so that we delete the end ranges
* first to maintain index consistency.
*/
qsort(indices, 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_NewWideIntObj(x));
Tcl_ListObjAppendElement(interp, listObj, Tcl_NewWideIntObj(y));
Tcl_ListObjAppendElement(interp, listObj, Tcl_NewWideIntObj(width));
Tcl_ListObjAppendElement(interp, listObj, Tcl_NewWideIntObj(height));
Tcl_ListObjAppendElement(interp, listObj, Tcl_NewWideIntObj(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;
TkSizeT 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 = TkGetStringFromObj(objv[i], &length);
if (length > 1 && name[0] == '-') {
if (strncmp("-displaychars", name, length) == 0) {
i++;
visible = 1;
name = TkGetStringFromObj(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, indexFromLine, indexFromByteOffset;
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;
}
indexFromLine = TkBTreeLinesTo(textPtr, indexFromPtr->linePtr);
indexFromByteOffset = indexFromPtr->byteIndex;
result = TextReplaceCmd(textPtr, interp, indexFromPtr,
indexToPtr, objc, objv, 0);
if (result == TCL_OK) {
/*
* Move the insertion position to the correct place.
*/
TkTextIndex indexTmp;
TkTextMakeByteIndex(textPtr->sharedTextPtr->tree, textPtr, indexFromLine,
indexFromByteOffset, &indexTmp);
TkTextIndexForwChars(NULL, &indexTmp,
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(TkTextRunAfterSyncCmd, (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, 0,
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:
if (textPtr->refCount-- <= 1) {
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. */
{
TkSharedText *sharedPtr = (TkSharedText *)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.
*/
if (sharedTextPtr->refCount-- > 1) {
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 = (TkTextSegment *)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 = (TkTextTag *)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;
Tcl_DeleteCommandFromToken(textPtr->interp, textPtr->widgetCmd);
if (textPtr->afterSyncCmd){
Tcl_DecrRefCount(textPtr->afterSyncCmd);
textPtr->afterSyncCmd = NULL;
}
if (textPtr->refCount-- <= 1) {
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. */
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) && (!Tcl_IsSafe(textPtr->interp));
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 != NULL)
|| (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) && (!Tcl_IsSafe(textPtr->interp))) {
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 = (TkText *)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. */
XEvent *eventPtr) /* Information about event. */
{
TkText *textPtr = (TkText *)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 = (TkText *)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;
TkSizeT length;
TkText *tPtr;
int *lineAndByteIndex;
int resetViewCount;
int pixels[2*PIXEL_CLIENTS];
const char *string = TkGetStringFromObj(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 = (int *)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, and send an event
* that the selection changed if that is the case.
*/
for (tPtr = sharedTextPtr->peers; tPtr != NULL ; tPtr = tPtr->next) {
if (TkBTreeCharTagged(indexPtr, tPtr->selTagPtr)) {
TkTextSelectionEvent(tPtr);
}
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;
char lMarkName[20] = "tk::undoMarkL";
char rMarkName[20] = "tk::undoMarkR";
char stringUndoMarkId[16] = "";
/*
* 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();
Tcl_Obj *markSetLUndoMarkCmdObj = Tcl_NewObj();
Tcl_Obj *markSetRUndoMarkCmdObj = NULL;
Tcl_Obj *markGravityLUndoMarkCmdObj = Tcl_NewObj();
Tcl_Obj *markGravityRUndoMarkCmdObj = NULL;
/*
* 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);
Tcl_ListObjAppendElement(NULL, markSetLUndoMarkCmdObj,
Tcl_NewStringObj(Tk_PathName(textPtr->tkwin), -1));
Tcl_ListObjAppendElement(NULL, markSetLUndoMarkCmdObj,
Tcl_NewStringObj("mark", 4));
Tcl_ListObjAppendElement(NULL, markSetLUndoMarkCmdObj,
Tcl_NewStringObj("set", 3));
markSetRUndoMarkCmdObj = Tcl_DuplicateObj(markSetLUndoMarkCmdObj);
textPtr->sharedTextPtr->undoMarkId++;
sprintf(stringUndoMarkId, "%d", textPtr->sharedTextPtr->undoMarkId);
strcat(lMarkName, stringUndoMarkId);
strcat(rMarkName, stringUndoMarkId);
Tcl_ListObjAppendElement(NULL, markSetLUndoMarkCmdObj,
Tcl_NewStringObj(lMarkName, -1));
Tcl_ListObjAppendElement(NULL, markSetRUndoMarkCmdObj,
Tcl_NewStringObj(rMarkName, -1));
Tcl_ListObjAppendElement(NULL, markSetLUndoMarkCmdObj, index1Obj);
Tcl_ListObjAppendElement(NULL, markSetRUndoMarkCmdObj, index2Obj);
Tcl_ListObjAppendElement(NULL, markGravityLUndoMarkCmdObj,
Tcl_NewStringObj(Tk_PathName(textPtr->tkwin), -1));
Tcl_ListObjAppendElement(NULL, markGravityLUndoMarkCmdObj,
Tcl_NewStringObj("mark", 4));
Tcl_ListObjAppendElement(NULL, markGravityLUndoMarkCmdObj,
Tcl_NewStringObj("gravity", 7));
markGravityRUndoMarkCmdObj = Tcl_DuplicateObj(markGravityLUndoMarkCmdObj);
Tcl_ListObjAppendElement(NULL, markGravityLUndoMarkCmdObj,
Tcl_NewStringObj(lMarkName, -1));
Tcl_ListObjAppendElement(NULL, markGravityRUndoMarkCmdObj,
Tcl_NewStringObj(rMarkName, -1));
Tcl_ListObjAppendElement(NULL, markGravityLUndoMarkCmdObj,
Tcl_NewStringObj("left", 4));
Tcl_ListObjAppendElement(NULL, markGravityRUndoMarkCmdObj,
Tcl_NewStringObj("right", 5));
/*
* 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);
TkUndoMakeCmdSubAtom(NULL, markSetLUndoMarkCmdObj, iAtom);
TkUndoMakeCmdSubAtom(NULL, markSetRUndoMarkCmdObj, iAtom);
TkUndoMakeCmdSubAtom(NULL, markGravityLUndoMarkCmdObj, iAtom);
TkUndoMakeCmdSubAtom(NULL, markGravityRUndoMarkCmdObj, iAtom);
dAtom = TkUndoMakeSubAtom(&TextUndoRedoCallback, textPtr->sharedTextPtr,
deleteCmdObj, NULL);
TkUndoMakeCmdSubAtom(NULL, markSet1InsertObj, dAtom);
TkUndoMakeCmdSubAtom(NULL, seeInsertObj, dAtom);
TkUndoMakeCmdSubAtom(NULL, markSetLUndoMarkCmdObj, dAtom);
TkUndoMakeCmdSubAtom(NULL, markSetRUndoMarkCmdObj, dAtom);
TkUndoMakeCmdSubAtom(NULL, markGravityLUndoMarkCmdObj, dAtom);
TkUndoMakeCmdSubAtom(NULL, markGravityRUndoMarkCmdObj, 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 = (TkSharedText *)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 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];
Tcl_HashSearch search;
Tcl_HashEntry *hPtr;
int i;
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);
}
}
/*
* 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.
*/
for (i=0, hPtr=Tcl_FirstHashEntry(&sharedTextPtr->tagTable, &search);
hPtr != NULL; i++, hPtr = Tcl_NextHashEntry(&search)) {
TkBTreeTag(&index1, &index2, (TkTextTag *)Tcl_GetHashValue(hPtr), 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 = (int *)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 TkSizeT
TextFetchSelection(
ClientData clientData, /* Information about text widget. */
TkSizeT offset, /* Offset within selection of first character
* to be returned. */
char *buffer, /* Location in which to place selection. */
TkSizeT maxBytes) /* Maximum number of bytes to place at buffer,
* not including terminating NULL
* character. */
{
TkText *textPtr = (TkText *)clientData;
TkTextIndex eof;
int count, chunkSize;
TkSizeT offsetInSeg;
TkTextSearch search;
TkTextSegment *segPtr;
if ((!textPtr->exportSelection) || Tcl_IsSafe(textPtr->interp)) {
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 > (int)maxBytes) {
chunkSize = (int)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,
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. */
{
TkText *textPtr = (TkText *)clientData;
if (TkpAlwaysShowSelection(textPtr->tkwin)) {
TkTextIndex start, end;
if ((!textPtr->exportSelection) || Tcl_IsSafe(textPtr->interp)) {
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. */
{
TkText *textPtr = (TkText *)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 {
TK_TEXT_SEARCH_HIDDEN,
TK_TEXT_SEARCH_END, TK_TEXT_SEARCH_ALL, TK_TEXT_SEARCH_BACK, TK_TEXT_SEARCH_COUNT, TK_TEXT_SEARCH_ELIDE,
TK_TEXT_SEARCH_EXACT, TK_TEXT_SEARCH_FWD, TK_TEXT_SEARCH_NOCASE,
TK_TEXT_SEARCH_NOLINESTOP, TK_TEXT_SEARCH_OVERLAP, TK_TEXT_SEARCH_REGEXP, TK_TEXT_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 TK_TEXT_SEARCH_END:
i++;
goto endOfSwitchProcessing;
case TK_TEXT_SEARCH_ALL:
searchSpec.all = 1;
break;
case TK_TEXT_SEARCH_BACK:
searchSpec.backwards = 1;
break;
case TK_TEXT_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 TK_TEXT_SEARCH_ELIDE:
case TK_TEXT_SEARCH_HIDDEN:
searchSpec.searchElide = 1;
break;
case TK_TEXT_SEARCH_EXACT:
searchSpec.exact = 1;
break;
case TK_TEXT_SEARCH_FWD:
searchSpec.backwards = 0;
break;
case TK_TEXT_SEARCH_NOCASE:
searchSpec.noCase = 1;
break;
case TK_TEXT_SEARCH_NOLINESTOP:
searchSpec.noLineStop = 1;
break;
case TK_TEXT_SEARCH_OVERLAP:
searchSpec.overlap = 1;
break;
case TK_TEXT_SEARCH_STRICTLIMITS:
searchSpec.strictLimits = 1;
break;
case TK_TEXT_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. */
TkSizeT *offsetPosPtr) /* For returning the text offset in the
* line. */
{
const TkTextIndex *indexPtr;
int line;
TkText *textPtr = (TkText *)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 TkSizeT
TextSearchIndexInLine(
const SearchSpec *searchSpecPtr,
/* Search parameters. */
TkTextLine *linePtr, /* The line we're looking at. */
TkSizeT byteIndex) /* Index into the line. */
{
TkTextSegment *segPtr;
TkTextIndex curIndex;
TkSizeT index;
int leftToScan;
TkText *textPtr = (TkText *)searchSpecPtr->clientData;
index = 0;
curIndex.tree = textPtr->sharedTextPtr->tree;
curIndex.linePtr = linePtr; curIndex.byteIndex = 0;
for (segPtr = linePtr->segPtr, leftToScan = byteIndex;
leftToScan + 1 > 1;
curIndex.byteIndex += segPtr->size, segPtr = segPtr->nextPtr) {
if ((segPtr->typePtr == &tkTextCharType) &&
(searchSpecPtr->searchElide
|| !TkTextIsElided(textPtr, &curIndex, NULL))) {
if (leftToScan + 1 < (int)segPtr->size + 1) {
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 = (TkText *)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) {
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. */
TkSizeT matchOffset, /* Offset of found item in utf-8 bytes for
* exact search, Unicode chars for regexp. */
TkSizeT matchLength) /* Length also in bytes/chars as per search
* type. */
{
TkSizeT numChars;
int leftToScan;
TkTextIndex curIndex, foundIndex;
TkTextSegment *segPtr;
TkTextLine *linePtr;
TkText *textPtr = (TkText *)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 + 1 >= searchSpecPtr->stopOffset1 + 1)) {
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 + 1) > searchSpecPtr->stopOffset1 + 1)) {
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 = (TkTextLine *)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 -= (int)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_NewWideIntObj(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;
int 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 = (TkTextTabArray *)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.
*/
TkUtfToUniChar(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(
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 {
TkSizeT length;
const char *str;
if (TkTextGetObjIndex(interp, textPtr, objv[arg], &index2) != TCL_OK) {
return TCL_ERROR;
}
str = TkGetStringFromObj(objv[arg], &length);
if (strncmp(str, "end", 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 = (char *)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 = (const char *)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 == 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;
(void)what;
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 {
TkSizeT 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;
Tcl_Obj *cmdObj;
int code;
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;
/*
* Convert undo/redo temporary marks set by TkUndoRevert() into
* indices left in the interp result.
*/
cmdObj = Tcl_ObjPrintf("::tk::TextUndoRedoProcessMarks %s",
Tk_PathName(textPtr->tkwin));
Tcl_IncrRefCount(cmdObj);
code = Tcl_EvalObjEx(textPtr->interp, cmdObj, TCL_EVAL_GLOBAL);
if (code != TCL_OK) {
Tcl_AddErrorInfo(textPtr->interp,
"\n (on undoing)");
Tcl_BackgroundException(textPtr->interp, code);
}
Tcl_DecrRefCount(cmdObj);
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;
Tcl_Obj *cmdObj;
int code;
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;
/*
* Convert undo/redo temporary marks set by TkUndoApply() into
* indices left in the interp result.
*/
cmdObj = Tcl_ObjPrintf("::tk::TextUndoRedoProcessMarks %s",
Tk_PathName(textPtr->tkwin));
Tcl_IncrRefCount(cmdObj);
code = Tcl_EvalObjEx(textPtr->interp, cmdObj, TCL_EVAL_GLOBAL);
if (code != TCL_OK) {
Tcl_AddErrorInfo(textPtr->interp,
"\n (on undoing)");
Tcl_BackgroundException(textPtr->interp, code);
}
Tcl_DecrRefCount(cmdObj);
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).
*
* 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) {
TkSizeT 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);
}
}
/*
*----------------------------------------------------------------------
*
* TkTextRunAfterSyncCmd --
*
* 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.
*
*----------------------------------------------------------------------
*/
void
TkTextRunAfterSyncCmd(
ClientData clientData) /* Information about text widget. */
{
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-- <= 1) {
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_BackgroundException(textPtr->interp, TCL_ERROR);
}
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->startOffset1) != TCL_OK) {
return TCL_ERROR;
}
/*
* Find the optional end location, similarly.
*/
if (toPtr != NULL) {
const TkTextIndex *indexToPtr, *indexFromPtr;
TkText *textPtr = (TkText *)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->stopOffset1) != 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;
TkSizeT 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;
TkSizeT smArray[2 * LOTS_OF_MATCHES];
TkSizeT *storeMatch = smArray;
TkSizeT *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 = TkGetStringFromObj(patObj, &matchLength);
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->stopOffset1;
} else {
firstOffset = 0;
}
if (alreadySearchOffset >= 0) {
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->startOffset1 + 1 > (TkSizeT)firstOffset + 1) {
firstOffset = searchSpecPtr->startOffset1;
}
if ((firstOffset >= lastOffset)
&& ((lastOffset != 0) || searchSpecPtr->exact)) {
goto nextLine;
}
} else {
/*
* Use only the first part of the line.
*/
if (searchSpecPtr->startOffset1 + 1 < (TkSizeT)lastOffset + 1) {
lastOffset = searchSpecPtr->startOffset1;
}
}
}
/*
* 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 = TCL_INDEX_NONE;
if (searchSpecPtr->exact) {
int maxExtraLines = 0;
const char *startOfLine = Tcl_GetString(theLine);
CLANG_ASSERT(pattern);
do {
int ch;
const char *p;
TkSizeT lastFullLine = lastOffset;
if (firstNewLine == -1) {
if (searchSpecPtr->strictLimits
&& (firstOffset + matchLength + 1 > (TkSizeT)lastOffset + 1)) {
/*
* 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 = matchLength ? pattern[0] : '\0';
if (alreadySearchOffset >= 0) {
p = startOfLine + alreadySearchOffset;
alreadySearchOffset = -1;
} else {
p = startOfLine + lastOffset -1;
}
while (p >= startOfLine + firstOffset) {
if (matchLength == 0 || (p[0] == c && !strncmp(
p, pattern, 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, 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 ((TkSizeT)lastTotal - skipFirst + 1 >= matchLength + 1) {
/*
* We now have enough text to match, so we
* make a final test and break whatever the
* result.
*/
if (strncmp(p, pattern, 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 ? matchLength : 1);
if (alreadySearchOffset < 0) {
break;
}
}
} else {
firstOffset = matchLength ? p - startOfLine + matchLength
: p - startOfLine + (TkSizeT)1;
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 +
TkUtfToUniChar(startOfLine+matchOffset,&ch);
}
}
} while (searchSpecPtr->all);
} else {
int maxExtraLines = 0;
int matches = 0;
int lastNonOverlap = -1;
do {
Tcl_RegExpInfo info;
int match;
TkSizeT 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 == (TkSizeT) (lastOffset - firstOffset)))) {
int extraLines = 0;
TkSizeT 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 == TCL_INDEX_NONE) {
/*
* 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 != (TkSizeT) lastTotal &&
firstOffset + info.matches[0].end + 1 < prevFullLine + 1)
|| info.extendStart == TCL_INDEX_NONE) {
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 + 1 >= (TkSizeT) lastOffset + 1)) {
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 ((TkSizeT)thisOffset - lastNonOverlap >=
lastBackwardsMatchOffset + matchLength + 1){
/*
* 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 == TCL_INDEX_NONE ||
((searchSpecPtr->all || searchSpecPtr->backwards)
&& (((TkSizeT)firstOffset + 1 < matchOffset + 1)
|| ((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.
*/
TkSizeT *newArray = (TkSizeT *)
ckalloc(4 * matchNum * sizeof(TkSizeT));
memcpy(newArray, storeMatch, matchNum*sizeof(TkSizeT));
memcpy(newArray + 2*matchNum, storeLength,
matchNum * sizeof(TkSizeT));
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 + 1 > 1 && !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] + 1
>= matchOffset + matchLength + 1) {
/*
* 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 != TCL_INDEX_NONE)
&& !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 dummy,
Tk_Window tkwin,
char *recordPtr, /* Pointer to widget record. */
TkSizeT internalOffset) /* Offset within *recordPtr containing the
* line value. */
{
TkTextLine *linePtr = *(TkTextLine **)(recordPtr + internalOffset);
(void)dummy;
(void)tkwin;
if ((internalOffset == TCL_INDEX_NONE) || (recordPtr == NULL)) {
return Tcl_NewObj();
}
return Tcl_NewWideIntObj(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 dummy,
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. */
TkSizeT 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;
(void)dummy;
(void)tkwin;
if (internalOffset != TCL_INDEX_NONE) {
internalPtr = (char *)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 dummy,
Tk_Window tkwin,
char *internalPtr, /* Pointer to storage for value. */
char *oldInternalPtr) /* Pointer to old value. */
{
(void)dummy;
(void)tkwin;
*(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) {
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 dummy, /* 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;
(void)dummy;
if (objc < 3) {
return TCL_ERROR;
}
if (Tcl_GetCommandInfo(interp, Tcl_GetString(objv[1]), &info) == 0) {
return TCL_ERROR;
}
textPtr = (TkText *)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:
*/