/*
* Copyright (C) 2013 FooProject
* * 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 3 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 .
*/
//#define TEST_ASYNC
using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Linq;
namespace FooEditEngine
{
///
/// 進行状況を表す列挙体
///
public enum ProgressState
{
///
/// 操作が開始したことを表す
///
Start,
///
/// 操作が終了したことを表す
///
Complete,
}
///
/// 進行状況を表すためのイベントデータ
///
public sealed class ProgressEventArgs : EventArgs
{
///
/// 進行状況
///
public ProgressState state;
///
/// コンストラクター
///
/// ProgressStateオブジェクト
public ProgressEventArgs(ProgressState state)
{
this.state = state;
}
}
///
/// 進行状況を通知するためのデリゲート
///
/// 送信元クラス
/// イベントデータ
public delegate void ProgressEventHandler(object sender, ProgressEventArgs e);
///
/// 更新タイプを表す列挙体
///
public enum UpdateType
{
///
/// ドキュメントが置き換えられたことを表す
///
Replace,
///
/// ドキュメント全体が削除されたことを表す
///
Clear,
}
///
/// 更新タイプを通知するためのイベントデータ
///
public sealed class DocumentUpdateEventArgs : EventArgs
{
///
/// 更新タイプ
///
public UpdateType type;
///
/// 開始位置
///
public int startIndex;
///
/// 削除された長さ
///
public int removeLength;
///
/// 追加された長さ
///
public int insertLength;
///
/// 更新イベントが発生した行。行が不明な場合や行をまたぐ場合はnullを指定すること。
///
public int? row;
///
/// コンストラクター
///
/// 更新タイプ
/// 開始インデックス
/// 削除された長さ
/// 追加された長さ
/// 開始行。nullを指定することができる
public DocumentUpdateEventArgs(UpdateType type, int startIndex, int removeLength, int insertLength, int? row = null)
{
this.type = type;
this.startIndex = startIndex;
this.removeLength = removeLength;
this.insertLength = insertLength;
this.row = row;
}
}
///
/// ドキュメントに更新があったことを伝えるためのデリゲート
///
/// 送信元クラス
/// イベントデータ
public delegate void DocumentUpdateEventHandler(object sender, DocumentUpdateEventArgs e);
///
/// ドキュメントの管理を行う
///
/// この型のすべてのメソッド・プロパティはスレッドセーフです
public sealed class Document : IEnumerable, IRandomEnumrator
{
const int MaxSemaphoreCount = 1;
Regex regex;
Match match;
StringBuffer buffer;
LineToIndexTable _LayoutLines;
bool _EnableFireUpdateEvent = true;
SemaphoreSlim Semaphore = new SemaphoreSlim(MaxSemaphoreCount);
///
/// コンストラクター
///
internal Document()
: this(null)
{
}
internal Document(Document doc)
{
if (doc == null)
this.buffer = new StringBuffer();
else
this.buffer = new StringBuffer(doc.buffer);
this.buffer.Update = new DocumentUpdateEventHandler(buffer_Update);
this.UpdateCalledAlways += (s, e) => { };
this.Update += new DocumentUpdateEventHandler((s, e) => { });
this.ChangeFireUpdateEvent += new EventHandler((s, e) => { });
this.Markers = new MarkerCollection(this);
this.UndoManager = new UndoManager();
this._LayoutLines = new LineToIndexTable(this);
this._LayoutLines.SpilitString = LayoutLines_SpilitStringByChar;
this._LayoutLines.Clear();
}
///
/// レイアウト行を表す
///
public LineToIndexTable LayoutLines
{
get
{
return this._LayoutLines;
}
}
IList LayoutLines_SpilitStringByChar(object sender, SpilitStringEventArgs e)
{
return this.CreateLineList(e.index, e.length);
}
///
/// レイアウト行を返す
///
/// 開始インデックス
/// 長さ
/// 1行当たりの最大文字数。-1で無制限
/// レイアウト行リスト
internal IList CreateLineList(int index, int length, int lineLimitLength = -1)
{
int startIndex = index;
int endIndex = index + length - 1;
List output = new List();
foreach (Tuple range in this.ForEachLines(startIndex, endIndex, lineLimitLength))
{
int lineHeadIndex = range.Item1;
int lineLength = range.Item2;
char c = this.buffer[lineHeadIndex + lineLength - 1];
bool hasNewLine = c == Document.NewLine;
output.Add(this.LayoutLines.CreateLineToIndexTableData(lineHeadIndex, lineLength, hasNewLine, null));
}
if (output.Count > 0)
output.Last().LineEnd = true;
return output;
}
internal void FireUpdate(DocumentUpdateEventArgs e)
{
this.buffer_Update(this.buffer, e);
}
///
/// ドキュメントが更新された時に呼ばれるイベント
///
public event DocumentUpdateEventHandler Update;
///
/// ドキュメントが更新された時に呼びされるイベント
///
///
/// FireUpdateEventの値に関わらず常に呼びされます
///
internal event DocumentUpdateEventHandler UpdateCalledAlways;
///
/// FireUpdateEventの値が変わったときに呼び出されるイベント
///
public event EventHandler ChangeFireUpdateEvent;
///
/// 改行コードの内部表現
///
public const char NewLine = '\n';
///
/// EOFの内部表現
///
public const char EndOfFile = '\u001a';
///
/// ロック中なら真を返し、そうでないなら偽を返す
///
public bool IsLocked
{
get
{
return this.Semaphore.CurrentCount == 0;
}
}
///
/// アンドゥ管理クラスを表す
///
public UndoManager UndoManager
{
get;
private set;
}
///
/// 文字列の長さ
///
public int Length
{
get
{
return this.buffer.Length;
}
}
///
/// 変更のたびにUpdateイベントを発生させるかどうか
///
public bool FireUpdateEvent
{
get
{
return this._EnableFireUpdateEvent;
}
set
{
this._EnableFireUpdateEvent = value;
this.ChangeFireUpdateEvent(this, null);
}
}
///
/// インデクサー
///
/// インデックス(自然数でなければならない)
/// Char型
public char this[int i]
{
get
{
return this.buffer[i];
}
}
///
/// マーカーコレクション
///
public MarkerCollection Markers
{
get;
private set;
}
internal StringBuffer StringBuffer
{
get
{
return this.buffer;
}
}
///
/// DocumentReaderを作成します
///
/// DocumentReaderオブジェクト
public DocumentReader CreateReader()
{
return new DocumentReader(this.buffer);
}
///
/// ロックを解除します
///
public void UnLock()
{
this.Semaphore.Release();
}
///
/// ロックします
///
public void Lock()
{
this.Semaphore.Wait();
}
///
/// ロックします
///
/// Taskオブジェクト
public Task LockAsync()
{
return this.Semaphore.WaitAsync();
}
///
/// マーカーを設定する
///
/// マーカーID
/// 設定したいマーカー
public void SetMarker(int id,Marker m)
{
if (m.start < 0 || m.start + m.length > this.Length)
throw new ArgumentOutOfRangeException("startもしくはendが指定できる範囲を超えています");
this.Markers.Add(id,m);
}
///
/// マーカーを削除する
///
/// マーカーID
/// 開始インデックス
/// 削除する長さ
public void RemoveMarker(int id,int start, int length)
{
if (start < 0 || start + length > this.Length)
throw new ArgumentOutOfRangeException("startもしくはendが指定できる範囲を超えています");
this.Markers.RemoveAll(id,start, length);
}
///
/// マーカーを削除する
///
/// マーカーID
/// 削除したいマーカーのタイプ
public void RemoveMarker(int id, HilightType type)
{
this.Markers.RemoveAll(id,type);
}
///
/// インデックスに対応するマーカーを得る
///
/// マーカーID
/// インデックス
/// Marker構造体の列挙子
public IEnumerable GetMarkers(int id, int index)
{
if (index < 0 || index > this.Length)
throw new ArgumentOutOfRangeException("indexが範囲を超えています");
return this.Markers.Get(id,index);
}
///
/// 部分文字列を取得する
///
/// 開始インデックス
/// 長さ
/// Stringオブジェクト
public string ToString(int index, int length)
{
return this.buffer.ToString(index, length);
}
///
/// インデックスを開始位置とする文字列を返す
///
/// 開始インデックス
/// Stringオブジェクト
public string ToString(int index)
{
return this.ToString(index, this.buffer.Length - index);
}
///
/// 行を取得する
///
/// 開始インデックス
/// 終了インデックス
/// 最大長
/// 行イテレーターが返される
public IEnumerable GetLines(int startIndex, int endIndex, int maxCharCount = -1)
{
return this.buffer.GetLines(startIndex, endIndex, maxCharCount);
}
internal IEnumerable> ForEachLines(int startIndex, int endIndex, int maxCharCount = -1)
{
return this.buffer.ForEachLines(startIndex, endIndex, maxCharCount);
}
///
/// 文字列を追加する
///
/// 追加したい文字列
/// 非同期操作中はこのメソッドを実行することはできません
public void Append(string s)
{
this.Replace(this.buffer.Length, 0, s);
}
///
/// 文字列を挿入する
///
/// 開始インデックス
/// 追加したい文字列
/// 読み出し操作中はこのメソッドを実行することはできません
public void Insert(int index, string s)
{
this.Replace(index, 0, s);
}
///
/// 文字列を削除する
///
/// 開始インデックス
/// 長さ
/// 読み出し操作中はこのメソッドを実行することはできません
public void Remove(int index, int length)
{
this.Replace(index, length, "");
}
///
/// ドキュメントを置き換える
///
/// 開始インデックス
/// 長さ
/// 文字列
/// 読み出し操作中はこのメソッドを実行することはできません
public void Replace(int index, int length, string s)
{
if (index < 0 || index > this.buffer.Length || index + length > this.buffer.Length || length < 0)
throw new ArgumentOutOfRangeException();
if (length == 0 && (s == string.Empty || s == null))
return;
foreach(int id in this.Markers.IDs)
this.RemoveMarker(id,index, length);
ReplaceCommand cmd = new ReplaceCommand(this.buffer, index, length, s);
this.UndoManager.push(cmd);
cmd.redo();
}
///
/// 物理行をすべて削除する
///
/// Dirtyフラグも同時にクリアーされます
/// 非同期操作中はこのメソッドを実行することはできません
public void Clear()
{
this.buffer.Clear();
}
///
/// ストリームからドキュメントを非同期的に構築します
///
/// IStreamReaderオブジェクト
/// キャンセルトークン
/// Taskオブジェクト
///
/// 読み取り操作は別スレッドで行われます。
/// また、非同期操作中はこのメソッドを実行することはできません。
///
internal async Task LoadAsync(IStreamReader fs, CancellationTokenSource tokenSource = null)
{
if (fs.IsEnd())
return;
try
{
await this.LockAsync().ConfigureAwait(false);
this.Clear();
this.FireUpdateEvent = false;
await this.buffer.LoadAsync(fs, tokenSource);
}
finally
{
this.FireUpdateEvent = true;
this.UnLock();
}
}
///
/// ストリームに非同期モードで保存します
///
/// IStreamWriterオブジェクト
/// キャンセルトークン
/// Taskオブジェクト
/// 非同期操作中はこのメソッドを実行することはできません
internal async Task SaveAsync(IStreamWriter fs, CancellationTokenSource tokenSource = null)
{
try
{
await this.LockAsync().ConfigureAwait(false);
StringBuilder line = new StringBuilder();
for (int i = 0; i < this.Length; i++)
{
char c = this[i];
line.Append(c);
if (c == Document.NewLine || i == this.Length - 1)
{
string str = line.ToString();
str = str.Replace(Document.NewLine.ToString(), fs.NewLine);
await fs.WriteAsync(str).ConfigureAwait(false);
line.Clear();
if (tokenSource != null)
tokenSource.Token.ThrowIfCancellationRequested();
#if TEST_ASYNC
System.Threading.Thread.Sleep(10);
#endif
}
}
}
finally
{
this.UnLock();
}
}
///
/// Find()およびReplaceAll()で使用するパラメーターをセットします
///
/// 検索したい文字列
/// 正規表現を使用するなら真
/// RegexOptions列挙体
public void SetFindParam(string pattern, bool UseRegex, RegexOptions opt)
{
this.match = null;
if (UseRegex)
this.regex = new Regex(pattern, opt);
else
this.regex = new Regex(Regex.Escape(pattern), opt);
}
///
/// 現在の検索パラメーターでWatchDogを生成する
///
/// ハイライトタイプ
/// 色
/// WatchDogオブジェクト
public RegexMarkerPattern CreateWatchDogByFindParam(HilightType type,Color color)
{
if (this.regex == null)
throw new InvalidOperationException("SetFindParam()を呼び出してください");
return new RegexMarkerPattern(this.regex,type,color);
}
///
/// 指定した文字列を検索します
///
/// 見つかった場合はSearchResult列挙子を返却します
/// 見つかったパターン以外を置き換えた場合、正常に動作しないことがあります
public IEnumerator Find()
{
return this.Find(0, this.Length);
}
///
/// 指定した文字列を検索します
///
/// 見つかった場合はSearchResult列挙子を返却します
/// 開始インデックス
/// 検索する長さ
/// 見つかったパターン以外を置き換えた場合、正常に動作しないことがあります
public IEnumerator Find(int start, int length)
{
if (this.regex == null)
throw new InvalidOperationException();
if (start < 0 || start >= this.Length)
throw new ArgumentOutOfRangeException();
int end = start + length - 1;
if(end > this.Length - 1)
throw new ArgumentOutOfRangeException();
StringBuilder line = new StringBuilder();
int oldLength = this.Length;
for (int i = start; i <= end; i++)
{
char c = this[i];
line.Append(c);
if (c == Document.NewLine || i == end)
{
this.match = this.regex.Match(line.ToString());
while (this.match.Success)
{
int startIndex = i - line.Length + 1 + this.match.Index;
int endIndex = startIndex + this.match.Length - 1;
yield return new SearchResult(this.match, startIndex, endIndex);
if (this.Length != oldLength) //長さが変わった場合は置き換え後のパターンの終点+1まで戻る
{
int delta = this.Length - oldLength;
i = endIndex + delta;
end = end + delta;
oldLength = this.Length;
break;
}
this.match = this.match.NextMatch();
}
line.Clear();
}
}
}
///
/// 任意のパターンですべて置き換えます
///
/// 置き換え後のパターン
/// グループ置き換えを行うなら真。そうでないなら偽
public void ReplaceAll(string replacePattern,bool groupReplace)
{
if (this.regex == null)
throw new InvalidOperationException();
ReplaceAllCommand cmd = new ReplaceAllCommand(this.buffer, this.LayoutLines, this.regex, replacePattern, groupReplace);
this.UndoManager.push(cmd);
cmd.redo();
}
///
/// 任意のパターンで置き換える
///
/// 対象となる文字列
/// 置き換え後の文字列
/// 大文字も文字を区別しないなら真。そうでないなら偽
///
/// 検索時に大文字小文字を区別します。また、このメソッドでは正規表現を使用することはできません
///
public void ReplaceAll2(string target, string pattern,bool ci = false)
{
FastReplaceAllCommand cmd = new FastReplaceAllCommand(this.buffer, this.LayoutLines, target, pattern,ci);
this.UndoManager.push(cmd);
cmd.redo();
}
#region IEnumerable メンバー
///
/// 列挙子を返します
///
/// IEnumeratorオブジェクトを返す
public IEnumerator GetEnumerator()
{
return this.buffer.GetEnumerator();
}
#endregion
#region IEnumerable メンバー
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
throw new NotImplementedException();
}
#endregion
void buffer_Update(object sender, DocumentUpdateEventArgs e)
{
switch (e.type)
{
case UpdateType.Replace:
if (e.row == null)
this._LayoutLines.UpdateAsReplace(e.startIndex, e.removeLength, e.insertLength);
else
this._LayoutLines.UpdateLineAsReplace(e.row.Value, e.removeLength, e.insertLength);
break;
case UpdateType.Clear:
this._LayoutLines.Clear();
break;
}
this.UpdateCalledAlways(this, e);
if(this.FireUpdateEvent)
this.Update(this, e);
}
}
public interface IStreamReader
{
///
/// ストリームが空かどうかを返す
///
bool IsEnd();
///
/// ストリームから行を読み取った物を返す。LoadAsyncを呼び出す場合は必ず実装してください
///
Task ReadLineAsync();
///
/// ストリームから指定した文字数だけ読み取る
///
/// 書き込み先バッファー
/// 書き込み先バッファーのインデックス
/// 読み取る文字数
/// 読み取った文字数
Task ReadAsync(char[] buffer, int index, int count);
}
public interface IStreamWriter
{
///
/// ストリームに書き込む。SaveAsyncを呼び出す場合は必ず実装してください
///
Task WriteAsync(string str);
///
/// 書き込む際に使用する改行コード
///
string NewLine
{
get;
set;
}
}
///
/// 検索結果を表す
///
public class SearchResult
{
private Match Match;
///
/// 一致した場所の開始位置を表す
///
public int Start;
///
/// 一致した場所の終了位置を表す
///
public int End;
///
/// 見つかった文字列を返す
///
public string Value
{
get { return this.Match.Value; }
}
///
/// 指定したパターンを置き換えて返す
///
/// 置き換える文字列
/// 置き換え後の文字列
public string Result(string replacement)
{
return this.Match.Result(replacement);
}
///
/// コンストラクター
///
/// Matchオブジェクト
/// 開始インデックス
/// 終了インデックス
public SearchResult(Match m, int start,int end)
{
this.Match = m;
this.Start = start;
this.End = end;
}
}
///
/// ドキュメントリーダー
///
public class DocumentReader : TextReader
{
StringBuffer document;
int currentIndex;
///
/// コンストラクター
///
///
internal DocumentReader(StringBuffer doc)
{
if (doc == null)
throw new ArgumentNullException();
this.document = doc;
}
///
/// 文字を取得する
///
/// 文字。取得できない場合は-1
public override int Peek()
{
if (this.document == null)
throw new InvalidOperationException();
if (this.currentIndex >= this.document.Length)
return -1;
return this.document[this.currentIndex];
}
///
/// 文字を取得し、イテレーターを一つ進める
///
/// 文字。取得できない場合は-1
public override int Read()
{
int c = this.Peek();
if(c != -1)
this.currentIndex++;
return c;
}
///
/// 文字列を読み取りバッファーに書き込む
///
/// バッファー
/// 開始インデックス
/// カウント
/// 読み取られた文字数
public override int Read(char[] buffer, int index, int count)
{
if (this.document == null)
throw new InvalidOperationException();
if (buffer == null)
throw new ArgumentNullException();
if (this.document.Length < count)
throw new ArgumentException();
if (index < 0 || count < 0)
throw new ArgumentOutOfRangeException();
if (this.document.Length == 0)
return 0;
int actualCount = count;
if (index + count - 1 > this.document.Length - 1)
actualCount = this.document.Length - index;
string str = this.document.ToString(index, actualCount);
for (int i = 0; i < str.Length; i++) //ToCharArray()だと戻った時に消えてしまう
buffer[i] = str[i];
this.currentIndex = index + actualCount;
return actualCount;
}
///
/// オブジェクトを破棄する
///
/// 真ならアンマネージドリソースを解放する
protected override void Dispose(bool disposing)
{
this.document = null;
}
}
}