/***************************************************************************

  gdocument.cpp

  (c) 2000-2007 Benoit Minisini <gambas@users.sourceforge.net>

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 1, or (at your option)
  any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

***************************************************************************/

#define __G_DOCUMENT_CPP

#include <ctype.h>

#include "main.h"
#include "gview.h"
#include "gdocument.h"

#define GMAX(x, y) ((x) > (y) ? (x) : (y))
#define GMIN(x, y) ((x) < (y) ? (x) : (y))

/**---- GLine -----------------------------------------------------------*/

GLine::GLine()
{
	s = "";
	state = Normal;
	tag = 0;
	flag = 0;
	modified = false;
	changed = false;
	baptized = false;
	proc = false;
	highlight = NULL;
}

GLine::~GLine()
{
	GB.FreeArray(&highlight);
}

/**---- GCommand -----------------------------------------------------------*/

class GCommand
{
public:
  enum Type
  {
    None, Begin, End, Insert, Delete, Indent, Unindent
  };

  virtual ~GCommand() { }
  virtual Type type() { return None; }
  virtual int nest() { return 0; }
  virtual void print() { }
  virtual bool merge(GCommand *) { return false; }
  virtual void process(GDocument *doc, bool undo) { }
};

class GBeginCommand: public GCommand
{
public:
  GBeginCommand() { }
  Type type() { return Begin; }
  void print() { qDebug("Begin"); }
  int nest() { return 1; }
};

class GEndCommand: public GCommand
{
public:
  GEndCommand() { }
  Type type() { return End; }
  void print() { qDebug("End"); }
  int nest() { return -1; }
  bool merge(GCommand *o) { return (o->type() == Begin); }
};

class GDeleteCommand: public GCommand
{
public:
  int x, y, x2, y2;
  GString str;

  GDeleteCommand(int y, int x, int y2, int x2, const GString & str)
  {
    this->x = x; this->y = y; this->x2 = x2; this->y2 = y2; this->str = str;
  }

  Type type() { return Delete; }
  void print() { qDebug("Delete: (%d %d)-(%d %d): '%s'", x, y, x2, y2, str.utf8()); }

  bool merge(GCommand *c)
  {
    if (c->type() != type())
      return false;

    GDeleteCommand *o = (GDeleteCommand *)c;

    if (x2 != o->x || y2 != o->y)
      return false;

    o->str.prepend(str);
    o->x = x;
    o->y = y;
    return true;
  }

  void process(GDocument *doc, bool undo)
  {
    if (undo)
      doc->insert(y, x, str);
    else
      doc->remove(y, x, y2, x2);
  }
};

class GInsertCommand: public GDeleteCommand
{
public:
  GInsertCommand(int y, int x, int y2, int x2, const GString & str): GDeleteCommand(y, x, y2, x2, str) {}
  Type type() { return Insert; }
  void print() { qDebug("Insert: (%d %d)-(%d %d): '%s'", x, y, x2, y2, str.utf8()); }

  bool merge(GCommand *c)
  {
    if (c->type() != type())
      return false;

    if (str.isNewLine(0))
      return false;

    GInsertCommand *o = (GInsertCommand *)c;

    if (o->str.length() && o->str.isNewLine(str.length() - 1))
      return false;

    if (x != o->x2 || y != o->y2)
      return false;

    o->str += str;
    o->x2 = x2;
    o->y2 = y2;
    return true;
  }

  void process(GDocument *doc, bool undo)
  {
    GDeleteCommand::process(doc, !undo);
  }
};


/**---- GDocument -----------------------------------------------------------*/

#define FOR_EACH_VIEW(_view) \
  for (GEditor *_view = views.first(); _view ; _view = views.next())

void GDocument::init()
{
  selector = NULL;
  baptismLimit = 0;
}

GDocument::GDocument()
{
  oldCount = 0;
  blockUndo = false;
  tabWidth = 2;
  readOnly = false;
  highlightMode = None;

  //lines = new GArray<GLine>;
  lines.setAutoDelete(true);

  undoList.setAutoDelete(true);
  redoList.setAutoDelete(true);

  clear();
  //setText(""); //GString("Gambas\nAlmost\nMeans\nBASIC!\n"));
}

GDocument::~GDocument()
{
}

void GDocument::clear()
{
  uint i;

  clearUndo();
  lines.clear();
  lines.append(new GLine);
  init();

  updateViews();

  for (i = 0; i < views.count(); i++)
    views.at(i)->cursorGoto(0, 0, false);
}


void GDocument::reset()
{
  for (uint i = 0; i < lines.count(); i++)
    lines.at(i)->changed = false;

  updateViews();
}


GString GDocument::getText()
{
  GString tmp;

  if (lines.count())
  {
    for (uint i = 0; i < lines.count(); i++)
      colorize(i);

    for (uint i = 0; i < (lines.count() - 1); i++)
    {
      tmp += lines.at(i)->s;
      tmp += '\n';
		}

    tmp += lines.at(lines.count() - 1)->s;

    updateViews();
  }

  return tmp;
}

GString GDocument::getSelectedText() const
{
  GString tmp;
  int x1, y1, x2, y2;

  if (lines.count() && hasSelection())
  {
    getSelection(&y1, &x1, &y2, &x2);
    if (y1 == y2)
      tmp = lines.at(y1)->s.mid(x1, x2 - x1);
    else
    {
      tmp = lines.at(y1)->s.mid(x1);
      tmp += '\n';
      for (int i = y1 + 1; i < y2; i++)
      {
        tmp += lines.at(i)->s;
        tmp += '\n';
			}

      tmp += lines.at(y2)->s.left(x2);
    }
  }

  return tmp;
}

int GDocument::getIndent(int y, bool *empty)
{
  int i;
  GString s = lines.at(y)->s;
  bool e = true;

  for (i = 0; i < (int)s.length(); i++)
  {
    if (!s.isSpace(i))
    {
      e = false;
      break;
    }
  }

  if (empty)
    *empty = e;

  return i;
}

void GDocument::updateLineWidth(int y)
{
	FOR_EACH_VIEW(v)
	{
		v->updateWidth(y);
	}
}

void GDocument::insertLine(int y)
{
	lines.insert(y, new GLine());
	lines.at(y)->modified = lines.at(y)->changed = true;
	if (baptismLimit > y)
		baptismLimit++;
	FOR_EACH_VIEW(v) { v->lineInserted(y); }
}

void GDocument::insert(int y, int x, const GString & text)
{
  int pos = 0;
  int pos2;
  int xs = x, ys = y;
  GLine *l;
  int n = 1;
  GString rest;
  int yy;

  if (readOnly)
  {
    xAfter = x;
    yAfter = y;
    return;
  }

	FOR_EACH_VIEW(v)
	{
		v->nx = v->x;
		v->ny = v->y;
	}

	while (y >= (int)lines.count())
	{
		yy = (int)lines.count();
		insertLine(yy);
	}

  for(;;)
  {
    pos2 = text.find('\n', pos);
    if (pos2 < 0)
      pos2 = text.length();

    l = lines.at(y);

    if (pos2 > pos)
    {
      l->s.insert(x, text.mid(pos, pos2 - pos));
      l->modified = l->changed = true;

      //maxLength = GMAX(maxLength, (int)l->s.length());
      updateLineWidth(y);

      FOR_EACH_VIEW(v)
      {
        if (v->ny == y && v->nx >= x)
          v->nx += pos2 - pos;
      }

      x += pos2 - pos;
    }

    pos = pos2 + 1;

    if (pos > (int)text.length())
      break;

    if (x < (int)l->s.length())
    {
      rest = l->s.mid(x);

      l->s.remove(x, rest.length());
      l->modified = l->changed = true;
      updateLineWidth(y);
    }

    FOR_EACH_VIEW(v)
    {
      if (v->ny >= y)
        v->ny++;
    }

    y++;
		
		insertLine(y);
    
    n = -1;
    x = 0;

  }

  if (n < 0 && rest.length())
  {
    l->s.insert(x, rest);
    l->modified = l->changed = true;

    //maxLength = GMAX(maxLength, (int)l->s.length());
    updateLineWidth(y);
  }

  updateViews(ys, n);

  addUndo(new GInsertCommand(ys, xs, y, x, text));

  yAfter = y;
  xAfter = x;

	emitTextChanged();

	FOR_EACH_VIEW(v)
	{
		v->cursorGoto(v->ny, v->nx, FALSE);
	}
}

void GDocument::removeLine(int y)
{
	lines.remove(y);
	if (baptismLimit > y)
		baptismLimit--;
	FOR_EACH_VIEW(v) { v->lineRemoved(y); }
}

void GDocument::remove(int y1, int x1, int y2, int x2)
{
  GLine *l;
  GString text, rest;
  int y;

  yAfter = y1;
  xAfter = x1;

  if (readOnly)
    return;

	FOR_EACH_VIEW(v)
	{
		v->nx = v->x;
		v->ny = v->y;
	}

  l = lines.at(y1);

  if (y1 == y2)
  {
    if (x2 >= x1)
    {
      text = l->s.mid(x1, x2 - x1);

      l->s.remove(x1, x2 - x1);
      l->modified = l->changed = true;
      updateLineWidth(y1);

      FOR_EACH_VIEW(v)
      {
        if (v->ny == y1 && v->nx > x1)
          v->nx = GMAX(x1, v->nx - (x2 - x1));
      }

      updateViews(y1);
    }
  }
  else
  {
    text = l->s.mid(x1) + '\n';
    rest = lines.at(y2)->s.left(x2);

    l->s = l->s.left(x1) + lines.at(y2)->s.mid(x2);
    l->modified = l->changed = true;
    l->state = 0; // force highlighting of next line.

    //maxLength = GMAX(maxLength, (int)l->s.length());
    updateLineWidth(y1);

    for (y = y1 + 1; y < y2; y++)
      text += lines.at(y)->s + '\n';
    text += rest;

    for (y = y1 + 1; y <= y2; y++)
    {
    	removeLine(y1 + 1);
		}

    FOR_EACH_VIEW(v)
    {
      if (v->ny > y1)
      {
        v->ny = GMAX(y1, v->ny - (y2 - y1));
        if (v->ny == y1)
          v->nx = x1;
      }
      else if (v->ny == y1 && v->nx > x1)
        v->nx = x1;
    }

    updateViews(y1, -1);
  }

  addUndo(new GDeleteCommand(y1, x1, y2, x2, text));

  emitTextChanged();

	FOR_EACH_VIEW(v)
	{
		v->cursorGoto(v->ny, v->nx, FALSE);
	}
}

void GDocument::setText(const GString & text)
{
  bool oldReadOnly = readOnly;

  readOnly = false;
  blockUndo = true;
  
  clear();
  insert(0, 0, text);
  colorize(0);
  reset();

  blockUndo = false;
  readOnly = oldReadOnly;

  FOR_EACH_VIEW(v)
    v->cursorGoto(0, 0, false);
}

void GDocument::unsubscribe(GEditor *view)
{
  //qDebug("unsubscribe %p -> %p", view, this);
  views.removeRef(view);
	if (views.count() == 0)
		delete this;
}

void GDocument::subscribe(GEditor *view)
{
  //qDebug("subscribe %p -> %p", view, this);
  views.removeRef(view);
  views.append(view);
  view->setNumRows(lines.count());
  view->updateContents();
}

void GDocument::updateViews(int row, int count)
{
  uint i;

  if (lines.count() > oldCount)
  {
    oldCount = lines.count();
    FOR_EACH_VIEW(v)
    {
      v->setNumRows(oldCount);
      v->updateHeight();
    }
  }

  /*if (maxLength != oldMaxLength)
  {
    oldMaxLength = maxLength;
    FOR_EACH_VIEW(v)
      v->updateLength();
  }*/

  if (row < 0)
  {
    row = 0;
    count = oldCount;
  }
  else if (count < 0)
  {
    count = oldCount - row;
  }

  count = GMIN((int)oldCount - row, count);

  FOR_EACH_VIEW(v)
  {
    for (i = row; i < (uint)(row + count); i++)
    {
        v->updateLine(i);
    }
  }

  if (lines.count() < oldCount)
  {
    oldCount = lines.count();
    FOR_EACH_VIEW(v)
    {
      v->setNumRows(oldCount);
      v->updateHeight();
    }
  }

  FOR_EACH_VIEW(v)
    v->checkMatching();
}


void GDocument::getSelection(int *y1, int *x1, int *y2, int *x2) const
{
  if (!selector)
    return;

  if (ys2 > ys || (ys2 == ys && xs2 > xs))
  {
    *y1 = ys;
    *y2 = ys2;
    if (x1) *x1 = xs;
    if (x2) *x2 = xs2;
  }
  else
  {
    *y1 = ys2;
    *y2 = ys;
    if (x1) *x1 = xs2;
    if (x2) *x2 = xs;
  }
}

void GDocument::startSelection(GEditor *view, int y, int x)
{
  hideSelection();
  ys = y;
  xs = x;
  ys2 = ys;
  xs2 = xs;
  selector = view;
  updateViews(y);
}

void GDocument::endSelection(int y, int x)
{
  int y1a, y2a, y1b, y2b;

  getSelection(&y1a, NULL, &y2a, NULL);
  ys2 = y;
  xs2 = x;
  getSelection(&y1b, NULL, &y2b, NULL);

  if (y1a == y1b)
    updateViews(GMIN(y2a, y2b), GMAX(y2a, y2b) - GMIN(y2a, y2b) + 1);
  else if (y2a == y2b)
    updateViews(GMIN(y1a, y1b), GMAX(y1a, y1b) - GMIN(y1a, y1b) + 1);
  else
    updateViews(GMIN(y1a, y1b), GMAX(y2a, y2b) - GMIN(y1a, y1b) + 1);

  updateViews(y);
}

void GDocument::hideSelection()
{
  int y1, y2;

  if (!selector)
    return;

  getSelection(&y1, NULL, &y2, NULL);
  selector = NULL;
  updateViews(y1, y2 - y1 + 1);
}

void GDocument::eraseSelection()
{
  int y1, y2, x1, x2;

  if (!selector)
    return;

  getSelection(&y1, &x1, &y2, &x2);
  selector = NULL;
  /*x2--;
  if (x2 < 0)
  {
    y2--;
    x2 = lines.at(y2)->s.length() - 1;
  }*/
  remove(y1, x1, y2, x2);
}

void GDocument::clearUndo()
{
  undoList.clear();
  redoList.clear();
  undoLevel = 0;
}

void GDocument::addUndo(GCommand *c)
{
  if (blockUndo)
    return;

  //qDebug("addUndo");
  //c->print();

  if (!undoList.isEmpty())
  {
    if (c->merge(undoList.last()))
    {
      //qDebug("merge");
      delete c;
      return;
    }
  }

  undoList.append(c);

  if (!redoList.isEmpty())
  {
    redoList.clear();
    //emit redoAvailable(false);
  }
}

void GDocument::begin()
{
	if (undoLevel == 0)
		textHasChanged = false;
	undoLevel++;
  addUndo(new GBeginCommand());
}

void GDocument::end()
{
	undoLevel--;
  addUndo(new GEndCommand());
  if (undoLevel == 0 && textHasChanged)
  	emitTextChanged();
}

void GDocument::addRedo(GCommand * c)
{
  if (blockUndo)
    return;

  redoList.append(c);
}

bool GDocument::undo()
{
  int nest;

  if (undoList.isEmpty() || isReadOnly() || blockUndo)
    return true;

  blockUndo = true;
  nest = 0;

  //qDebug("BEGIN UNDO");

	begin();

  do
  {
    GCommand *c = undoList.take();
    if (!c)
      break;
    //c->print();
    c->process(this, true);
    nest += c->nest();
    //if (d->undoList.isEmpty())
    //  emit undoAvailable(false);
    redoList.append(c);
  }
  while (nest);

  //qDebug("END UNDO");

	end();
  blockUndo = false;
  
  return false;
}

bool GDocument::redo()
{
  int nest;

  if (redoList.isEmpty() || isReadOnly() || blockUndo)
    return true;

  blockUndo = true;
  nest = 0;
  begin();

  do
  {
    GCommand *c = redoList.take();
    if (!c)
      break;
    c->process(this, false);
    nest += c->nest();

    /*if (d->redoList.isEmpty())
      emit redoAvailable(false);
    if (d->undoList.isEmpty())
      emit undoAvailable(true);*/
    undoList.append(c);
  }
  while (nest);
	
	end();
  blockUndo = false;
  
  return false;
}

int GDocument::wordLeft(int y, int x, bool word)
{
  int xw = x;
  GString s = lines.at(y)->s;

	if (!word)
	{
		while (xw > 0 && s.isSpace(xw - 1))
			xw--;
	}
	
	if (xw > 0)
	{
		if (s.isWordChar(xw - 1))
		{
			for(;;)
			{
				xw--;
				if (xw <= 0 || !s.isWordChar(xw - 1))
					break;
			}
		}
		else if (!word)
		{
			for(;;)
			{
				xw--;
				if (xw <= 0 || s.isWordChar(xw - 1) || s.isSpace(xw - 1))
					break;
			}
		}
	}

  return xw;
}

int GDocument::wordRight(int y, int x, bool word)
{
  int xw = x;
  GString s = lines.at(y)->s;
  int len = s.length();

	if (xw < len)
	{
		if (s.isWordChar(xw))
		{
			for(;;)
			{
				xw++;
				if (xw >= len || !s.isWordChar(xw))
					break;
			}
		}
		else if (!word)
		{
			for(;;)
			{
				xw++;
				if (xw >= len || s.isWordChar(xw) || s.isSpace(xw))
					break;
			}
		}
	}

	if (!word)
	{
		while (xw < len && s.isSpace(xw))
			xw++;
	}
	
  return xw;
}

int GDocument::getLength() const
{
  uint i, len;

  if (lines.count())
    return 0;

  len = 0;

  for (i = 0; i < lines.count(); i++)
    len += lines.at(i)->s.length() + 1;

  return (len - 1);
}


GString GDocument::getLine(int y) const
{
  GString tmp;

  if (y >= 0 || y < (int)lines.count())
    tmp = lines.at(y)->s;

  return tmp;
}

void GDocument::setLine(int y, GString & str)
{
  if (y < 0 || y >= (int)lines.count())
    return;

	begin();
	if (lines.at(y)->s.length())
		remove(y, 0, y, lines.at(y)->s.length());
	if (str.length())
		insert(y, 0, str);
  end();
  updateViews(y);
}

bool GDocument::getLineFlag(int y, int f) const
{
  if (y >= 0 || y < (int)lines.count())
    return lines.at(y)->flag & (1 << f);
  else
    return false;
}

void GDocument::setLineFlag(int y, int f, bool b)
{
  if (y < 0 || y >= (int)lines.count())
    return;

  if (b)
    lines.at(y)->flag |= (1 << f);
  else
    lines.at(y)->flag &= ~(1 << f);

  updateViews(y);
}

int GDocument::convState(int state)
{
  switch(state)
  {
    case EVAL_TYPE_END: return GLine::Normal;
    case EVAL_TYPE_RESERVED: return GLine::Keyword;
    case EVAL_TYPE_IDENTIFIER: return GLine::Symbol;
    case EVAL_TYPE_CLASS: return GLine::Datatype;
    case EVAL_TYPE_NUMBER: return GLine::Number;
    case EVAL_TYPE_STRING: return GLine::String;
    case EVAL_TYPE_SUBR: return GLine::Subr;
    case EVAL_TYPE_COMMENT: return GLine::Comment;
    case EVAL_TYPE_OPERATOR: return GLine::Operator;
    case EVAL_TYPE_DATATYPE: return GLine::Datatype;
    case EVAL_TYPE_ERROR: return GLine::Error;
    default: return GLine::Normal;
  }
}

void GDocument::highlightGambas(GEditor *, uint &state, int &tag, GString &s, GHighlightArray *data, bool &proc)
{
  const char *src;
  EVAL_ANALYZE result;
  int i;
  uint ls;

  ls = s.length();
  src = (const char *)s.utf8();

  EVAL.Analyze(src, strlen(src), &result);

	GB.NewArray(data, sizeof(GHighlight), result.len);

  for (i = 0; i < result.len; i++)
  {
    //qDebug("state = %d -> %d  len = %d", result.color[i].state, QEditor::convState[result.color[i].state], result.color[i].len);
    (*data)[i].state = convState(result.color[i].state);
    (*data)[i].len = result.color[i].len;
  }

  s = result.str;
  GB.FreeString(&result.str);

  proc = result.proc;
}

#if 0
bool GDocument::highlightTest(GEditor *, uint &state, GString &s, GHighlightArray &data, bool &proc)
{
	QMemArray<uchar> d(s.length());
	uint i;
	int j;
	uint nstate = 0;

	if (s.length() == 0)
	{
		proc = false;
		return false;
	}

	for (i = 0; i < s.length(); i++)
	{
		nstate = state;

		if (state == GLine::Normal)
		{
			if (s[i] == '/' && i < (s.length() - 1) && s[i + 1] == '*')
				state = nstate = GLine::Comment;
		}
		else if (state == GLine::Comment)
		{
			if (s[i] == '/' && i > 0 && s[i - 1] == '*')
				nstate = GLine::Normal;
		}

		d[i] = state;
		state = nstate;
	}

  j = -1;
  data.resize(s.length());

  for (i = 0; i < s.length(); i++)
  {
  	if (i == 0 || d[i] != data[j].state)
  	{
  		j++;
  		data[j].state = d[i];
  		data[j].len = 1;
		}
		else
			data[j].len++;
  }

	if (j >= 0)
  	data.resize(j + 1);

  proc = false;

  return false; //qstrcmp((const char *)cs, result.str);
}
#endif

void GDocument::colorize(int y)
{
  GLine *l;
  //bool modif;
  bool proc;
  GString old;
  uint state;
  int tag;
  int nupd = 0;
  bool changed = false;
  
  if (highlightMode == None)
    return;

  if (y < 0)
    return;

	while (y < numLines())
	{
		l = lines.at(y);

		if (!l->modified)
			break;

		//qDebug("colorize: %d: %s", y, l->s.utf8());

		nupd++;
		//modif = false;

		if (y == 0)
		{
			state = GLine::Normal;
			tag = 0;
		}
		else
		{
			state = lines.at(y - 1)->state;
			tag = lines.at(y - 1)->tag;
		}

		if (l->s.length())
		{
			old = l->s;
			GB.FreeArray(&l->highlight);
			(*highlightCallback)(views.first(), state, tag, l->s, &l->highlight, proc);
			l->proc = proc;

			if (old != l->s)
			{
				begin();
				addUndo(new GDeleteCommand(y, 0, y, old.length(), old));
				if (l->s.length())
					addUndo(new GInsertCommand(y, 0, y, l->s.length(), l->s));
				end();
				
				//maxLength = GMAX(maxLength, (int)l->s.length());
				updateLineWidth(y);
				//qDebug("colorize: %d has changed: '%s' -> '%s'", y, old.utf8(), l->s.utf8());
				l->changed = true;
				changed = true;
			}
		}
		else
		{
			GB.FreeArray(&l->highlight);
			l->proc = false;
		}

		l->modified = false;

		state &= 0xF;
		tag &= 0xFFFF;

		if (l->state == state && l->tag == tag)
			break;

		l->state = state;
		l->tag = tag;
		
		y++;
		if (y < numLines())
			lines.at(y)->modified = true;
	}

	if (changed)
		emitTextChanged();
	
	if (nupd >= 1)
		updateViews(y - nupd + 1, nupd);
}


void GDocument::baptizeUntil(int y)
{
	/*bool busy = (y - baptismLimit) > 256;

	if (busy)
  	qApp->setOverrideCursor(Qt::waitCursor);*/

	while (baptismLimit <= y)
	{
		colorize(baptismLimit);
		baptismLimit++;
	}

	/*if (busy)
  	qApp->restoreOverrideCursor();*/
}

void GDocument::setHighlightMode(int mode, GHighlightCallback cb)
{
	int i;

	highlightMode = mode;

	if (mode == Gambas)
		highlightCallback = GDocument::highlightGambas;
	else
		highlightCallback = cb;

	for (i = 0; i < numLines(); i++)
	{
		lines.at(i)->modified = true;
		lines.at(i)->baptized = false;
	}

	baptismLimit = 0;

	updateViews();
}


void GDocument::emitTextChanged()
{
	if (undoLevel > 0)
	{
		textHasChanged = true;
		return;
	}
	
	//qDebug("emitTextChanged");
	
	FOR_EACH_VIEW(v)
	{
		v->docTextChanged();
	}
}

