/* * 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; } } }