/* GNU moe - My Own Editor
   Copyright (C) 2005-2024 Antonio Diaz Diaz.

   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 2 of the License, 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, see <http://www.gnu.org/licenses/>.
*/

#include <algorithm>
#include <cerrno>
#include <climits>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <vector>
#include <unistd.h>
#include <sys/stat.h>

#include "buffer.h"
#include "buffer_handle.h"
#include "block.h"
#include "iso_8859.h"
#include "menu.h"
#include "rc.h"
#include "screen.h"


namespace {

const std::vector< Point * > & get_marks_and_pointers( Buffer & b )
  {
  static std::vector< Point * > v;
  v.clear();
  for( int i = 0; i < 10; ++i ) v.push_back( &b.mark[i] );
  for( int i = 0; i < Bufhandle_vector::handles(); ++i )
    {
    Buffer_handle & bh = Bufhandle_vector::handle( i );
    if( &b == &bh.buffer() ) v.push_back( &bh.pointer );
    }
  if( Block::bufferp() == &b )
    {
    v.push_back( &Block::anchor() );
    v.push_back( &Block::begin() );
    v.push_back( &Block::end() );
    }
  return v;
  }


void adjust_point( const Buffer & b, Point & p, const Point & p1,
                   const Point & p2_old, const Point & p2_new )
  {
  if( p <= p1 ) return;			// was before the block, ignore it
  if( p < p2_old )			// was in the block, keep in
    {
    if( p >= p2_new ) { p = p2_new; if( p > p1 ) b.pprev( p ); }
    else if( !b.pisvalid( p ) ) b.pvalid( p );
    }
  else					// was beyond the block
    {
    if( p.line == p2_old.line ) p.col += p2_new.col - p2_old.col;
    p.line += p2_new.line - p2_old.line;
    }
  }


// adjust marks and pointers after a block insert/delete/replace
void adjust_marks_and_pointers( Buffer & b, const Point & p1,
                                const Point & p2_old, const Point & p2_new,
                                const Point * const p_ignore = 0 )
  {
  const std::vector< Point * > & pv = get_marks_and_pointers( b );
  for( unsigned i = 0; i < pv.size(); ++i )
    if( *pv[i] > p1 && pv[i] != p_ignore )
      adjust_point( b, *pv[i], p1, p2_old, p2_new );
  }

} // end namespace


const std::string Buffer::unnamed( "(Unnamed)" );


bool Buffer::Change_history::Record::append_record( const Basic_buffer & buffer,
                                                    Record & r )
  {
  if( r.atom_vector.size() != 1 ) return false;
  Atom & a1 = atom_vector.back();
  Atom & a2 = r.atom_vector.back();

  if( !a1.bbp && !a2.bbp && a1.end == a2.begin )
    { a1.end = a2.end; return true; }		// consecutive insert
  if( a1.bbp && a2.bbp )
    {
    if( a1.begin == a1.end && a2.begin == a2.end )	// two deletes
      {
      if( a1.begin == a2.begin )		// delete at same place
        {
        Point p = a1.bbp->eof();
        a1.bbp->pputb( p, *a2.bbp, a2.bbp->bof(), a2.bbp->eof() );
        delete a2.bbp; a2.bbp = 0; return true;
        }
      if( a2.bbp->one_character() && buffer.pprev( a1.begin ) && a1.begin == a2.begin )
        {					// consecutive delete
        Point p = a1.bbp->bof();
        a1.bbp->pputb( p, *a2.bbp, a2.bbp->bof(), a2.bbp->eof() );
        a1.end = a1.begin; delete a2.bbp; a2.bbp = 0; return true;
        }
      else a1.begin = a1.end;
      }
    else if( a1.begin < a1.end && a2.begin < a2.end && a1.end == a2.begin )
      {						// consecutive overwrite
      Point p = a1.bbp->eof();
      a1.bbp->pputb( p, *a2.bbp, a2.bbp->bof(), a2.bbp->eof() );
      a1.end = a2.end; delete a2.bbp; a2.bbp = 0; return true;
      }
    }
  return false;
  }


void Buffer::Change_history::Record::delete_atoms()
  {
  for( unsigned i = atom_vector.size(); i > 0; )
    if( atom_vector[--i].bbp )
      delete atom_vector[i].bbp;
  atom_vector.clear();
  }


Buffer::Change_history::~Change_history()
  {
  for( unsigned i = record_vector.size(); i > 0; )
    record_vector[--i].delete_atoms();
  }


void Buffer::Change_history::add_record( const Basic_buffer & buffer,
                                         Record & r )
  {
  if( index + 1 < records() )
    {
    for( long i = records(); i > index + 1; )
      record_vector[--i].delete_atoms();
    record_vector.erase( record_vector.begin() + index + 1, record_vector.end() );
    appendable = false;
    }
  if( !records() ) { appendable = false; force_append = false; }
  if( ( appendable || force_append ) && record_vector.back().append_record( buffer, r ) )
    record_vector.back().after = r.after;
  else if( force_append )
    {
    std::vector< Atom > & av = record_vector.back().atom_vector;
    av.insert( av.end(), r.atom_vector.begin(), r.atom_vector.end() );
    record_vector.back().after = r.after;
    }
  else { record_vector.push_back( r ); ++index; }
  appendable = true; force_append = false;
  }


bool Buffer::Options::set_lmargin( const int l )
  {
  if( l >= 0 && l < rmargin_ ) { lmargin_ = l; return true; }
  return false;
  }


bool Buffer::Options::set_rmargin( const int r )
  {
  if( r > lmargin_ && r < INT_MAX / 2 ) { rmargin_ = r; return true; }
  if( r == -1 ) { rmargin_ = INT_MAX / 2 - 1; return true; }
  return false;
  }


/* 'namep' must be 0 or "-" for stdin to be read into buffer.
   A non-null but empty 'namep' ("") creates an empty unnamed buffer.
*/
Buffer::Buffer( const char * const namep, const bool read_only )
  : backuped( false ), modified_( false ), saved( false ), crlf( false ),
    existed( false )
  {
  FILE * f = 0;
  const bool valid_namep = ( namep && namep[0] );
  const bool is_stdin = !namep || ( namep[0] == '-' && namep[1] == 0 );
  errno = 0;
  if( is_stdin && !isatty( fileno( stdin ) ) ) f = stdin;
  else if( valid_namep )
    {
    name_ = namep;
    if( read_only ||
        ( !(f = std::fopen( name_.c_str(), "r+" )) && errno != ENOENT ) )
      { f = std::fopen( name_.c_str(), "r" ); options.read_only = true; }
    }
  else return;				// empty unnamed buffer

  if( !f )
    { if( errno == ENOENT ) return; else throw Error( "Error opening file" ); }
  if( isatty( fileno( f ) ) )
    { std::fclose( f ); throw Error( "Is a terminal" ); }

  const int bufsize = 65536;
  char buf[bufsize];
  bool aborted = false;
  const bool interactive = Screen::height() > 0;
  bool file_eof = true;
  bool long_line = false;
  while( file_eof )
    {
    const int size = std::fread( buf, 1, bufsize, f );
    if( size <= 0 ) break;
    int pos = 0;
    while( pos < size )
      {
      int newpos = pos;
      while( buf[newpos] != '\n' && newpos + 1 < size ) ++newpos;
      try { if( !add_line( &buf[pos], newpos - pos + 1 ) )
              { file_eof = false; long_line = true; } }
      catch( ... ) { file_eof = false; break; }
      pos = newpos + 1;
      }
    if( interactive && Screen::kbhit() >= 0 && Screen::wait_kbhit() == 3 )
      { file_eof = false; aborted = true; }
    }

  bool file_error = std::ferror( f );
  if( file_eof ) file_eof = std::feof( f );
  if( f != stdin ) { if( std::fclose( f ) != 0 ) file_error = true; }
  else if( !ttyname( 1 ) || !std::freopen( ttyname( 1 ), "r", stdin ) )
    throw Error( "Error opening terminal for input" );
  if( aborted ) throw Error( "Aborted" );
  if( file_error ) throw Error( "Error reading file" );
  if( long_line ) throw Error( "Line too long" );
  if( !file_eof ) throw Error( "Not enough memory" );
  crlf = detect_and_remove_cr();
  existed = true;
  set_marks();
  }


int Buffer::set_marks()		// return the number of new marks set
  {
  int c = 0;
  if( mark[0].line < 0 )
    {
    for( int i = 10; i < 80 && i < lines(); ++i )
      if( get_line( i ).compare( 0, 10, "#include \"" ) == 0 )
        { mark[0].line = i - 1; ++c; break; }
    if( mark[0].line < 0 )
      for( int i = 19; i < 30 && i < lines(); ++i )
        if( blank( i ) ) { mark[0].line = i; ++c; break; }
    if( mark[0].line < 0 && lines() > 22 ) { mark[0].line = 22; ++c; }
    }
  for( long i = 1; i < 10; ++i )
    {
    const long line =
      ( lines() <= 1000 ) ? 100 * i - 1 : ( lines() * i + 9 ) / 10 - 1;
    if( line >= lines() ) break;
    if( mark[i].line >= 0 )
      for( int j = i - 1; j >= 0; --j )
        if( mark[i] == mark[j] ) { mark[i].line = -1; break; }
    if( mark[i].line < 0 ) { mark[i].line = line; ++c; }
    }
  return c;
  }


/* *namep  name   save_to   action
     ""     ""       x      error              (no names)
     ""      x     true     error              (no name to save to)
     x     valid   false    save               (if name != *namep, save to)
   valid    ""     false    save_and_set_name  (if read_only, save to)
   valid     x     true     save_to            (save to another file)
*/
bool Buffer::save( const std::string * namep, bool save_to,
                   const bool append, Point p1, Point p2 ) const
  {
  const bool valid_namep = ( namep && namep->size() );
  const bool had_name = ( name_.size() > 0 );
  if( !valid_namep && !had_name ) return false;		// no names
  if( !valid_namep && save_to ) return false;		// no name to save to

  if( !save_to && valid_namep &&
      ( ( had_name && name_ != *namep ) || ( !had_name && options.read_only ) ) )
    save_to = true;
  if( !save_to && options.read_only ) return false;

  int mode = 0;
  struct stat st;
  if( append ) mode = S_IRUSR | S_IWUSR;
  else if( ( valid_namep && stat( namep->c_str(), &st ) == 0 ) ||
           ( had_name && ( !valid_namep || name_ != *namep ) &&
             stat( name_.c_str(), &st ) == 0 ) ) mode = st.st_mode;

  // ask if not saving to buffer's own file except for emergency save
  if( ( save_to || !had_name ) && !append )
    {
    FILE * f = std::fopen( namep->c_str(), "r+" );
    if( f )
      {
      std::fclose( f );
      int key = Screen::show_message( "File exists. Overwrite ? (y/N) ",
                                      true, true );
      if( key != 'y' && key != 'Y' ) return false;
      }
    }
  if( !save_to )			// fill missing name_ or namep
    {
    if( valid_namep ) { if( !had_name ) name_ = *namep; }
    else if( had_name ) namep = &name_;
    }
  if( !namep ) return false;		// always save to file name '*namep'

  if( !Menu::make_dirs( *namep ) )
    { if( !had_name ) name_.clear();
      throw Error( "Error creating intermediate directory" ); }
  if( !save_to && !saved && !append )
    {
    if( RC::editor_options().backup && !backuped )
      {
      std::string backname( *namep ); backname += '~';
      std::remove( backname.c_str() );
      std::rename( namep->c_str(), backname.c_str() );
      backuped = true;
      }
    else std::remove( namep->c_str() );
    }

  FILE * f = std::fopen( namep->c_str(), append ? "a" : "w" );
  if( !f )
    { if( !had_name ) { name_.clear(); } throw Error( "Error opening file" ); }
  if( mode ) fchmod( fileno( f ), mode );

  if( !this->pisvalid( p1 ) ) p1 = this->bof();
  if( !this->pisvalid( p2 ) ) p2 = this->eof();
  if( !crlf )
    for( Point p = p1; p < p2; ) { std::fputc( pgetc( p ), f ); }
  else for( Point p = p1; p < p2; )	// put a CR before each LF
    {
    const int ch = pgetc( p );
    if( ch == '\n' ) std::fputc( '\r', f );
    std::fputc( ch, f );
    }
  if( std::fclose( f ) != 0 )
    { if( !had_name ) { name_.clear(); } throw Error( "Error closing file" ); }
  if( !save_to )
    {
    for( long i = change_history.records() - 1; i >= 0; --i )
      if( change_history.record_vector[i].modified == false )
        { change_history.record_vector[i].modified = true; break; }
    reset_appendable();
    modified_ = false; saved = true;
    }
  return true;
  }


long Buffer::character_offset( const Point & p, int & percent ) const
  {
  long total = 0;

  for( long i = 0; i < p.line; ++i ) total += characters( i );
  long offset = total + p.col;
  if( crlf ) { offset += p.line; if( (*this)[p] == '\n' ) ++offset; }
  for( long i = p.line; i < last_line(); ++i ) total += characters( i );
  if( crlf ) total += last_line();
  total += eof().col;
  if( total > 0 ) percent = ( 100LL * offset ) / total;
  else percent = 0;
  return offset;
  }


// insert block b.[p1,p2) at p and move next
bool Buffer::pputb( Point & p, const Basic_buffer & b, const Point & p1,
                    const Point & p2, const bool save_end )
  {
  if( options.read_only || !pisvalid( p ) ||
      p1 >= p2 || !b.pisvalid( p1 ) || !b.pisvalid( p2 ) ) return false;
  const Point p0 = p;
  Basic_buffer::pputb( p, b, p1, p2 );
  Change_history::Record r( p0, save_end ? p : p0, modified_ );
  r.add_atom( Change_history::Atom( p0, p ) );
  change_history.add_record( *this, r );
  modified_ = true;
  adjust_marks_and_pointers( *this, p0, p0, p, &p );
  return true;
  }


// put char and move next
bool Buffer::pputc( Point & p, const unsigned char ch )
  {
  if( options.read_only || !pisvalid( p ) ) return false;
  Basic_buffer tmp;
  Point dummy = tmp.bof();
  if( ch == '\n' )
    {
    tmp.pputc( dummy, ch );
    if( options.auto_indent )
      {
      const Point pt = bot( p );
      if( pt.col > 0 && p.col >= pt.col )
        tmp.pputb( dummy, *this, bol( pt ), pt );
      }
    const bool result = pputb( p, tmp, tmp.bof(), tmp.eof(), true );
    reset_appendable();
    return result;
    }
  else if( options.lmargin() > 0 && p.col == 0 && p == eol( p ) )
    {
    for( int i = 0; i < options.lmargin(); ++i ) tmp.pputc( dummy, ' ' );
    tmp.pputc( dummy, ch );
    reset_appendable();
    return pputb( p, tmp, tmp.bof(), tmp.eof(), true );
    }
  else
    {
    if( options.overwrite && p < eol( p ) )
      {
      const Point p1( p.line, p.col + 1 );
      Change_history::Record r( p, p1, modified_ );
      Change_history::Atom a( p, p1, *this );
      Basic_buffer::pchgc( p, ch );
      a.end = p1; r.add_atom( a );
      change_history.add_record( *this, r );
      }
    else
      {
      const Point p0 = p;
      Basic_buffer::pputc( p, ch );
      Change_history::Record r( p0, p, modified_ );
      r.add_atom( Change_history::Atom( p0, p ) );
      change_history.add_record( *this, r );
      const std::vector< Point * > & pv = get_marks_and_pointers( *this );
      for( unsigned i = 0; i < pv.size(); ++i )
        if( pv[i]->line == p0.line && pv[i]->col > p0.col && pv[i] != &p )
          ++pv[i]->col;
      if( options.word_wrap && to_cursor( p ).col > options.rmargin() + 1 &&
          !ISO_8859::isblank( ch ) )
        {
        const Point pt = bot( p0 );
        Point p2 = p0;
        while( p2 > pt && pprev( p2 ) && !ISO_8859::isblank( (*this)[p2] ) );
        Point p1 = p2;
        while( p1 > pt && pprev( p1 ) && ISO_8859::isblank( (*this)[p1] ) );
        if( p1 >= pt && p1 < p2 && pnext( p1 ) && pnext( p2 ) )
          {
          Point p3 = p1;
          reset_appendable(); pdelb( p1, p2 );
          force_append(); pputc( p3, '\n' );
          if( !options.auto_indent && options.lmargin() > 0 )
            {
            for( int i = 0; i < options.lmargin(); ++i ) tmp.pputc( dummy, ' ' );
            force_append(); pputb( p3, tmp, tmp.bof(), tmp.eof(), true );
            }
          for( unsigned i = 0; i < pv.size(); ++i )	// adjust p not in pv
            { if( pv[i] == &p ) break;
              if( i + 1 == pv.size() ) adjust_point( *this, p, p1, p2, p3 ); }
          }
        }
      }
    modified_ = true;
    }
  return true;
  }


// delete block at [p1,p2)
bool Buffer::pdelb( const Point & p1, const Point & p2, const bool save_end )
  {
  if( options.read_only || p1 >= p2 || !pisvalid( p1 ) || !pisvalid( p2 ) )
    return false;
  Change_history::Record r( save_end ? p2 : p1, p1, modified_ );
  r.add_atom( Change_history::Atom( p1, p2, *this ) );
  change_history.add_record( *this, r );
  modified_ = true;
  Basic_buffer::pdelb( p1, p2 );
  adjust_marks_and_pointers( *this, p1, p2, p1 );
  return true;
  }


// delete char at 'p', or at '--p' if back is true
bool Buffer::pdelc( Point & p, const bool back )
  {
  Point p1 = p;
  if( back ) return pprev( p ) && pdelb( p, p1, true );
  return pnext( p1 ) && pdelb( p, p1 );
  }


// delete line and set 'p' to bol()
bool Buffer::pdell( Point & p )
  {
  Point p1 = eol( p );
  if( options.read_only || characters( p.line ) <= 0 ||
      ( p1 != eof() && !pnext( p1 ) ) ) return false;
  p.col = 0;
  return pdelb( p, p1 );
  }


// replace block at [p1,p2) with b.[bp1,bp2) and move p2 next
bool Buffer::replace( const Point & p1, Point & p2, const Basic_buffer & b,
                      const Point & bp1, const Point & bp2 )
  {
  if( options.read_only || p1 > p2 || !pisvalid( p1 ) || !pisvalid( p2 ) ||
      bp1 > bp2 || !b.pisvalid( bp1 ) || !b.pisvalid( bp2 ) ) return false;
  if( p1 == p2 ) return pputb( p2, b, bp1, bp2 );
  const Point p2_orig = p2;
  if( bp1 == bp2 ) { p2 = p1; return pdelb( p1, p2_orig ); }
  Change_history::Record r( p1, p1, modified_ );	// save begin
  Change_history::Atom a( p1, p2, *this );
  Basic_buffer::pdelb( p1, p2 );
  p2 = p1;
  Basic_buffer::pputb( p2, b, bp1, bp2 );
  a.end = p2;
//  Change_history::Record r( p2_orig, p2, modified_ );	// save end
  r.add_atom( a ); change_history.add_record( *this, r );
  modified_ = true;
  adjust_marks_and_pointers( *this, p1, p2_orig, p2, &p2 );
  return true;
  }


bool Buffer::undo( Point & p )
  {
  if( change_history.index < 0 ) return false;
  Screen::show_feedback( "undoing..." );
  Change_history::Record & rec = change_history.record_vector[change_history.index];
  for( unsigned i = rec.atom_vector.size(); i > 0; )
    {
    Change_history::Atom & atom = rec.atom_vector[--i];
    Basic_buffer * tmp = 0;
    const Point end = atom.end;
    if( atom.begin < atom.end )
      {
      tmp = new Basic_buffer( *this, atom.begin, atom.end );
      Basic_buffer::pdelb( atom.begin, atom.end );
      }
    atom.end = atom.begin;
    if( atom.bbp )
      {
      Basic_buffer::pputb( atom.end, *atom.bbp, atom.bbp->bof(), atom.bbp->eof() );
      delete atom.bbp;
      }
    atom.bbp = tmp;
    adjust_marks_and_pointers( *this, atom.begin, end, atom.end, &atom.end );
    }
  p = rec.before;
  std::swap( modified_, rec.modified );
  --change_history.index;
  return true;
  }


bool Buffer::redo( Point & p )
  {
  if( change_history.index + 1 >= change_history.records() ) return false;
  Screen::show_feedback( "redoing..." );
  ++change_history.index;
  Change_history::Record & rec = change_history.record_vector[change_history.index];
  for( unsigned i = 0; i < rec.atom_vector.size(); ++i )
    {
    Change_history::Atom & atom = rec.atom_vector[i];
    Basic_buffer * tmp = 0;
    const Point end = atom.end;
    if( atom.begin < atom.end )
      {
      tmp = new Basic_buffer( *this, atom.begin, atom.end );
      Basic_buffer::pdelb( atom.begin, atom.end );
      }
    atom.end = atom.begin;
    if( atom.bbp )
      {
      Basic_buffer::pputb( atom.end, *atom.bbp, atom.bbp->bof(), atom.bbp->eof() );
      delete atom.bbp;
      }
    atom.bbp = tmp;
    adjust_marks_and_pointers( *this, atom.begin, end, atom.end, &atom.end );
    }
  p = rec.after;
  std::swap( modified_, rec.modified );
  return true;
  }


/* Reformat paragraph around cursor.
   If change, move cursor to ( bop.line, lmargin ) and return true. */
bool Buffer::reformat_paragraph( Point & cursor, const bool append_change )
  {
  const Point bop = Basic_buffer::bop( cursor );	// begin of paragraph
  const Point eop = Basic_buffer::eop( cursor );	// end of paragraph
  if( bop >= eop ) return false;
  const Point bot = Basic_buffer::bot( bop );	// begin of text in paragraph
  const int rmargin = options.rmargin();
  const int lmargin = std::max( options.lmargin(),
                                (int)std::min( rmargin - 1L, cursor.col ) );
  Buffer tmp;
  tmp.options.set_rmargin( rmargin );
  tmp.options.word_wrap = true;
  Point dummy = tmp.bof();
  if( bop < bot && to_cursor( bot ).col > lmargin )
    tmp.Basic_buffer::pputb( dummy, *this, bop, bot );
  tmp.options.set_lmargin( lmargin );
  bool change = false;
  bool pending_nbsp = false;
  bool pending_space = false;
  bool period2 = false;		// prev char was period in 2 spaces mode
  for( Point p = bot; p < eop; )
    {
    unsigned char ch = pgetc( p );
    if( std::isspace( ch ) )		// 'std' to exclude no-break space
      {
      if( ( ch != ' ' && ch != '\n' ) || pending_nbsp ) change = true;
      pending_nbsp = false;	// ignore nbsp before normal whitespace
      pending_space = true;
      }
    else if( ch == 0xA0 )	// ignore nbsp after normal whitespace
      { if( pending_nbsp ) change = true;
        else if( !pending_space ) pending_nbsp = true; }
    else
      {
      if( pending_space )
        { pending_space = false; tmp.pputc( dummy, ' ' );
          if( period2 ) tmp.pputc( dummy, ' ' ); }
      else if( pending_nbsp )
        { pending_nbsp = false; tmp.pputc( dummy, 0xA0 ); }
      tmp.pputc( dummy, ch ); // if( dummy != tmp.eof() ) std::exit( 3 );
      period2 = ( ch == '.' && options.two_spaces );
      }
    if( Screen::kbhit() >= 0 && Screen::wait_kbhit() == 3 )
      throw Error( "Aborted" );
    }
  if( dummy != tmp.bol( dummy ) ) tmp.pputc( dummy, '\n' );
  if( !change ) change = ( tmp.last_line() != ( eop.line - bop.line ) );
  if( !change )
    for( long line = 0; line < tmp.lines(); ++line )
      if( tmp.characters( line ) != characters( bop.line + line ) &&
          ( !tmp.blank( line ) || !blank( bop.line + line ) ) )
        { change = true; break; }
  // detect changes that don't change total number of spaces in line
  if( !change && options.two_spaces )
    for( long line = 0; line < tmp.lines(); ++line )
      if( tmp.get_line( line ) != get_line( bop.line + line ) &&
          ( !tmp.blank( line ) || !blank( bop.line + line ) ) )
        { change = true; break; }
  if( change )
    {
    if( append_change ) force_append(); else reset_appendable();
    Point p = eop;
    replace( bop, p, tmp, tmp.bof(), tmp.eof() );
    cursor.line = bop.line; cursor.col = lmargin;
    }
  return change;
  }
