2 * Copyright (C) 2013 FooProject
\r
3 * * 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
\r
4 * the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
\r
6 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
\r
7 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
\r
9 You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
\r
12 //#define TEST_ASYNC
\r
16 using System.Collections.Generic;
\r
18 using System.Text.RegularExpressions;
\r
19 using System.Threading;
\r
20 using System.Threading.Tasks;
\r
23 namespace FooEditEngine
\r
28 public enum ProgressState
\r
40 /// 進行状況を表すためのイベントデータ
\r
42 public sealed class ProgressEventArgs : EventArgs
\r
47 public ProgressState state;
\r
51 /// <param name="state">ProgressStateオブジェクト</param>
\r
52 public ProgressEventArgs(ProgressState state)
\r
59 /// 進行状況を通知するためのデリゲート
\r
61 /// <param name="sender">送信元クラス</param>
\r
62 /// <param name="e">イベントデータ</param>
\r
63 public delegate void ProgressEventHandler(object sender, ProgressEventArgs e);
\r
68 public enum UpdateType
\r
71 /// ドキュメントが置き換えられたことを表す
\r
75 /// ドキュメント全体が削除されたことを表す
\r
81 /// 更新タイプを通知するためのイベントデータ
\r
83 public sealed class DocumentUpdateEventArgs : EventArgs
\r
88 public UpdateType type;
\r
92 public int startIndex;
\r
96 public int removeLength;
\r
100 public int insertLength;
\r
104 /// <param name="type">更新タイプ</param>
\r
105 /// <param name="startIndex">開始インデックス</param>
\r
106 /// <param name="removeLength">削除された長さ</param>
\r
107 /// <param name="insertLength">追加された長さ</param>
\r
108 public DocumentUpdateEventArgs(UpdateType type, int startIndex, int removeLength, int insertLength)
\r
111 this.startIndex = startIndex;
\r
112 this.removeLength = removeLength;
\r
113 this.insertLength = insertLength;
\r
118 /// ドキュメントに更新があったことを伝えるためのデリゲート
\r
120 /// <param name="sender">送信元クラス</param>
\r
121 /// <param name="e">イベントデータ</param>
\r
122 public delegate void DocumentUpdateEventHandler(object sender, DocumentUpdateEventArgs e);
\r
127 /// <remarks>この型のすべてのメソッド・プロパティはスレッドセーフです</remarks>
\r
128 public sealed class Document : IEnumerable<char>, IRandomEnumrator<char>
\r
130 const int MaxSemaphoreCount = 1;
\r
133 StringBuffer buffer;
\r
134 LineToIndexTable _LayoutLines;
\r
135 bool _EnableFireUpdateEvent = true;
\r
136 SemaphoreSlim Semaphore = new SemaphoreSlim(MaxSemaphoreCount);
\r
141 internal Document(ITextRender render)
\r
142 : this(null,render)
\r
146 internal Document(Document doc,ITextRender render)
\r
149 this.buffer = new StringBuffer();
\r
151 this.buffer = new StringBuffer(doc.buffer);
\r
152 this.buffer.Update += new DocumentUpdateEventHandler(buffer_Update);
\r
153 this.UpdateCalledAlways += (s, e) => { };
\r
154 this.Update += new DocumentUpdateEventHandler((s, e) => { });
\r
155 this.ChangeFireUpdateEvent += new EventHandler((s, e) => { });
\r
156 this.Markers = new MarkerCollection(this);
\r
157 this.UndoManager = new UndoManager();
\r
158 this._LayoutLines = new LineToIndexTable(this, render);
\r
159 this._LayoutLines.SpilitString = LayoutLines_SpilitStringByChar;
\r
160 this._LayoutLines.Clear();
\r
166 public LineToIndexTable LayoutLines
\r
170 return this._LayoutLines;
\r
174 IList<LineToIndexTableData> LayoutLines_SpilitStringByChar(object sender, SpilitStringEventArgs e)
\r
176 return this.CreateLineList(e.index, e.length);
\r
182 /// <param name="index">開始インデックス</param>
\r
183 /// <param name="length">長さ</param>
\r
184 /// <param name="lineLimitLength">1行当たりの最大文字数。-1で無制限</param>
\r
185 /// <returns>レイアウト行リスト</returns>
\r
186 internal IList<LineToIndexTableData> CreateLineList(int index, int length, int lineLimitLength = -1)
\r
188 int startIndex = index;
\r
189 int endIndex = index + length - 1;
\r
190 List<LineToIndexTableData> output = new List<LineToIndexTableData>();
\r
192 foreach (Tuple<int, int> range in this.ForEachLines(startIndex, endIndex, lineLimitLength))
\r
194 int lineHeadIndex = range.Item1;
\r
195 int lineLength = range.Item2;
\r
196 char c = this.buffer[lineHeadIndex + lineLength - 1];
\r
197 bool hasNewLine = c == Document.NewLine;
\r
198 output.Add(this.LayoutLines.CreateLineToIndexTableData(lineHeadIndex, lineLength, hasNewLine, null));
\r
201 if (output.Count > 0)
\r
202 output.Last().LineEnd = true;
\r
207 internal void FireUpdate(DocumentUpdateEventArgs e)
\r
209 this.buffer_Update(this.buffer, e);
\r
213 /// ドキュメントが更新された時に呼ばれるイベント
\r
215 public event DocumentUpdateEventHandler Update;
\r
218 /// ドキュメントが更新された時に呼びされるイベント
\r
221 /// FireUpdateEventの値に関わらず常に呼びされます
\r
223 internal event DocumentUpdateEventHandler UpdateCalledAlways;
\r
226 /// FireUpdateEventの値が変わったときに呼び出されるイベント
\r
228 public event EventHandler ChangeFireUpdateEvent;
\r
233 public const char NewLine = '\n';
\r
238 public const char EndOfFile = '\u001a';
\r
241 /// ロック中なら真を返し、そうでないなら偽を返す
\r
243 public bool IsLocked
\r
247 return this.Semaphore.CurrentCount == 0;
\r
254 public UndoManager UndoManager
\r
267 return this.buffer.Length;
\r
272 /// 変更のたびにUpdateイベントを発生させるかどうか
\r
274 public bool FireUpdateEvent
\r
278 return this._EnableFireUpdateEvent;
\r
282 this._EnableFireUpdateEvent = value;
\r
283 this.ChangeFireUpdateEvent(this, null);
\r
290 /// <param name="i">インデックス(自然数でなければならない)</param>
\r
291 /// <returns>Char型</returns>
\r
292 public char this[int i]
\r
296 return this.buffer[i];
\r
303 public MarkerCollection Markers
\r
309 internal StringBuffer StringBuffer
\r
313 return this.buffer;
\r
318 /// DocumentReaderを作成します
\r
320 /// <returns>DocumentReaderオブジェクト</returns>
\r
321 public DocumentReader CreateReader()
\r
323 return new DocumentReader(this.buffer);
\r
329 public void UnLock()
\r
331 this.Semaphore.Release();
\r
339 this.Semaphore.Wait();
\r
345 /// <returns>Taskオブジェクト</returns>
\r
346 public Task LockAsync()
\r
348 return this.Semaphore.WaitAsync();
\r
354 /// <param name="id">マーカーID</param>
\r
355 /// <param name="m">設定したいマーカー</param>
\r
356 public void SetMarker(int id,Marker m)
\r
358 if (m.start < 0 || m.start + m.length > this.Length)
\r
359 throw new ArgumentOutOfRangeException("startもしくはendが指定できる範囲を超えています");
\r
361 this.Markers.Add(id,m);
\r
367 /// <param name="id">マーカーID</param>
\r
368 /// <param name="start">開始インデックス</param>
\r
369 /// <param name="length">削除する長さ</param>
\r
370 public void RemoveMarker(int id,int start, int length)
\r
372 if (start < 0 || start + length > this.Length)
\r
373 throw new ArgumentOutOfRangeException("startもしくはendが指定できる範囲を超えています");
\r
375 this.Markers.RemoveAll(id,start, length);
\r
381 /// <param name="id">マーカーID</param>
\r
382 /// <param name="type">削除したいマーカーのタイプ</param>
\r
383 public void RemoveMarker(int id, HilightType type)
\r
385 this.Markers.RemoveAll(id,type);
\r
389 /// インデックスに対応するマーカーを得る
\r
391 /// <param name="id">マーカーID</param>
\r
392 /// <param name="index">インデックス</param>
\r
393 /// <returns>Marker構造体の列挙子</returns>
\r
394 public IEnumerable<Marker> GetMarkers(int id, int index)
\r
396 if (index < 0 || index > this.Length)
\r
397 throw new ArgumentOutOfRangeException("indexが範囲を超えています");
\r
398 return this.Markers.Get(id,index);
\r
404 /// <param name="index">開始インデックス</param>
\r
405 /// <param name="length">長さ</param>
\r
406 /// <returns>Stringオブジェクト</returns>
\r
407 public string ToString(int index, int length)
\r
409 return this.buffer.ToString(index, length);
\r
413 /// インデックスを開始位置とする文字列を返す
\r
415 /// <param name="index">開始インデックス</param>
\r
416 /// <returns>Stringオブジェクト</returns>
\r
417 public string ToString(int index)
\r
419 return this.ToString(index, this.buffer.Length - index);
\r
425 /// <param name="startIndex">開始インデックス</param>
\r
426 /// <param name="endIndex">終了インデックス</param>
\r
427 /// <param name="maxCharCount">最大長</param>
\r
428 /// <returns>行イテレーターが返される</returns>
\r
429 public IEnumerable<string> GetLines(int startIndex, int endIndex, int maxCharCount = -1)
\r
431 return this.buffer.GetLines(startIndex, endIndex, maxCharCount);
\r
434 internal IEnumerable<Tuple<int, int>> ForEachLines(int startIndex, int endIndex, int maxCharCount = -1)
\r
436 return this.buffer.ForEachLines(startIndex, endIndex, maxCharCount);
\r
443 /// <param name="s">追加したい文字列</param>
\r
444 /// <remarks>非同期操作中はこのメソッドを実行することはできません</remarks>
\r
445 public void Append(string s)
\r
447 this.Replace(this.buffer.Length, 0, s);
\r
453 /// <param name="index">開始インデックス</param>
\r
454 /// <param name="s">追加したい文字列</param>
\r
455 /// <remarks>読み出し操作中はこのメソッドを実行することはできません</remarks>
\r
456 public void Insert(int index, string s)
\r
458 this.Replace(index, 0, s);
\r
464 /// <param name="index">開始インデックス</param>
\r
465 /// <param name="length">長さ</param>
\r
466 /// <remarks>読み出し操作中はこのメソッドを実行することはできません</remarks>
\r
467 public void Remove(int index, int length)
\r
469 this.Replace(index, length, "");
\r
475 /// <param name="index">開始インデックス</param>
\r
476 /// <param name="length">長さ</param>
\r
477 /// <param name="s">文字列</param>
\r
478 /// <remarks>読み出し操作中はこのメソッドを実行することはできません</remarks>
\r
479 public void Replace(int index, int length, string s)
\r
481 if (index < 0 || index > this.buffer.Length || index + length > this.buffer.Length || length < 0)
\r
482 throw new ArgumentOutOfRangeException();
\r
483 if (length == 0 && (s == string.Empty || s == null))
\r
486 foreach(int id in this.Markers.IDs)
\r
487 this.RemoveMarker(id,index, length);
\r
489 ReplaceCommand cmd = new ReplaceCommand(this.buffer, index, length, s);
\r
490 this.UndoManager.push(cmd);
\r
497 /// <remarks>Dirtyフラグも同時にクリアーされます</remarks>
\r
498 /// <remarks>非同期操作中はこのメソッドを実行することはできません</remarks>
\r
499 public void Clear()
\r
501 this.buffer.Clear();
\r
505 /// ストリームからドキュメントを非同期的に構築します
\r
507 /// <param name="fs">IStreamReaderオブジェクト</param>
\r
508 /// <param name="tokenSource">キャンセルトークン</param>
\r
509 /// <returns>Taskオブジェクト</returns>
\r
511 /// 読み取り操作は別スレッドで行われます。
\r
512 /// また、非同期操作中はこのメソッドを実行することはできません。
\r
514 internal async Task LoadAsync(IStreamReader fs, CancellationTokenSource tokenSource = null)
\r
521 await this.LockAsync().ConfigureAwait(false);
\r
523 this.FireUpdateEvent = false;
\r
524 this.UndoManager.BeginLock();
\r
526 for (int i = 0; (str = await fs.ReadLineAsync().ConfigureAwait(false)) != null; i++)
\r
528 int index = this.Length;
\r
532 this.Replace(index, 0, str + Document.NewLine);
\r
534 if (tokenSource != null)
\r
535 tokenSource.Token.ThrowIfCancellationRequested();
\r
537 System.Threading.Thread.Sleep(10);
\r
543 this.FireUpdateEvent = true;
\r
544 this.UndoManager.EndLock();
\r
550 /// ストリームに非同期モードで保存します
\r
552 /// <param name="fs">IStreamWriterオブジェクト</param>
\r
553 /// <param name="tokenSource">キャンセルトークン</param>
\r
554 /// <returns>Taskオブジェクト</returns>
\r
555 /// <remarks>非同期操作中はこのメソッドを実行することはできません</remarks>
\r
556 internal async Task SaveAsync(IStreamWriter fs, CancellationTokenSource tokenSource = null)
\r
560 await this.LockAsync().ConfigureAwait(false);
\r
561 StringBuilder line = new StringBuilder();
\r
562 for (int i = 0; i < this.Length; i++)
\r
566 if (c == Document.NewLine || i == this.Length - 1)
\r
568 string str = line.ToString();
\r
569 str = str.Replace(Document.NewLine.ToString(), fs.NewLine);
\r
570 await fs.WriteAsync(str).ConfigureAwait(false);
\r
572 if (tokenSource != null)
\r
573 tokenSource.Token.ThrowIfCancellationRequested();
\r
575 System.Threading.Thread.Sleep(10);
\r
587 /// Find()およびReplaceAll()で使用するパラメーターをセットします
\r
589 /// <param name="pattern">検索したい文字列</param>
\r
590 /// <param name="UseRegex">正規表現を使用するなら真</param>
\r
591 /// <param name="opt">RegexOptions列挙体</param>
\r
592 public void SetFindParam(string pattern, bool UseRegex, RegexOptions opt)
\r
596 this.regex = new Regex(pattern, opt);
\r
598 this.regex = new Regex(Regex.Escape(pattern), opt);
\r
602 /// 現在の検索パラメーターでWatchDogを生成する
\r
604 /// <param name="type">ハイライトタイプ</param>
\r
605 /// <param name="color">色</param>
\r
606 /// <returns>WatchDogオブジェクト</returns>
\r
607 public RegexMarkerPattern CreateWatchDogByFindParam(HilightType type,Color color)
\r
609 if (this.regex == null)
\r
610 throw new InvalidOperationException("SetFindParam()を呼び出してください");
\r
611 return new RegexMarkerPattern(this.regex,type,color);
\r
617 /// <returns>見つかった場合はSearchResult列挙子を返却します</returns>
\r
618 /// <remarks>見つかったパターン以外を置き換えた場合、正常に動作しないことがあります</remarks>
\r
619 public IEnumerator<SearchResult> Find()
\r
621 return this.Find(0, this.Length);
\r
627 /// <returns>見つかった場合はSearchResult列挙子を返却します</returns>
\r
628 /// <param name="start">開始インデックス</param>
\r
629 /// <param name="length">検索する長さ</param>
\r
630 /// <remarks>見つかったパターン以外を置き換えた場合、正常に動作しないことがあります</remarks>
\r
631 public IEnumerator<SearchResult> Find(int start, int length)
\r
633 if (this.regex == null)
\r
634 throw new InvalidOperationException();
\r
635 if (start < 0 || start >= this.Length)
\r
636 throw new ArgumentOutOfRangeException();
\r
638 int end = start + length - 1;
\r
640 if(end > this.Length - 1)
\r
641 throw new ArgumentOutOfRangeException();
\r
643 StringBuilder line = new StringBuilder();
\r
644 int oldLength = this.Length;
\r
645 for (int i = start; i <= end; i++)
\r
649 if (c == Document.NewLine || i == end)
\r
651 this.match = this.regex.Match(line.ToString());
\r
652 while (this.match.Success)
\r
654 int startIndex = i - line.Length + 1 + this.match.Index;
\r
655 int endIndex = startIndex + this.match.Length - 1;
\r
657 yield return new SearchResult(this.match, startIndex, endIndex);
\r
659 if (this.Length != oldLength) //長さが変わった場合は置き換え後のパターンの終点+1まで戻る
\r
661 int delta = this.Length - oldLength;
\r
662 i = endIndex + delta;
\r
664 oldLength = this.Length;
\r
668 this.match = this.match.NextMatch();
\r
676 /// 任意のパターンですべて置き換えます
\r
678 /// <param name="replacePattern">置き換え後のパターン</param>
\r
679 /// <param name="groupReplace">グループ置き換えを行うなら真。そうでないなら偽</param>
\r
680 public void ReplaceAll(string replacePattern,bool groupReplace)
\r
682 if (this.regex == null)
\r
683 throw new InvalidOperationException();
\r
684 ReplaceAllCommand cmd = new ReplaceAllCommand(this.buffer, this.regex, replacePattern, groupReplace);
\r
685 this.UndoManager.push(cmd);
\r
692 /// <param name="target">対象となる文字列</param>
\r
693 /// <param name="pattern">置き換え後の文字列</param>
\r
694 /// <param name="ci">大文字も文字を区別しないなら真。そうでないなら偽</param>
\r
696 /// 検索時に大文字小文字を区別します。また、このメソッドでは正規表現を使用することはできません
\r
698 public void ReplaceAll2(string target, string pattern,bool ci = false)
\r
700 FastReplaceAllCommand cmd = new FastReplaceAllCommand(this.buffer, target, pattern,ci);
\r
701 this.UndoManager.push(cmd);
\r
705 #region IEnumerable<char> メンバー
\r
710 /// <returns>IEnumeratorオブジェクトを返す</returns>
\r
711 public IEnumerator<char> GetEnumerator()
\r
713 return this.buffer.GetEnumerator();
\r
718 #region IEnumerable メンバー
\r
720 System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
\r
722 throw new NotImplementedException();
\r
727 void buffer_Update(object sender, DocumentUpdateEventArgs e)
\r
731 case UpdateType.Replace:
\r
732 this._LayoutLines.UpdateAsReplace(e.startIndex, e.removeLength, e.insertLength);
\r
734 case UpdateType.Clear:
\r
735 this._LayoutLines.Clear();
\r
738 this.UpdateCalledAlways(this, e);
\r
739 if(this.FireUpdateEvent)
\r
740 this.Update(this, e);
\r
744 public interface IStreamReader
\r
752 /// ストリームから行を読み取った物を返す。LoadAsyncを呼び出す場合は必ず実装してください
\r
754 Task<string> ReadLineAsync();
\r
757 public interface IStreamWriter
\r
760 /// ストリームに書き込む。SaveAsyncを呼び出す場合は必ず実装してください
\r
762 Task WriteAsync(string str);
\r
765 /// 書き込む際に使用する改行コード
\r
777 public class SearchResult
\r
779 private Match Match;
\r
794 public string Value
\r
796 get { return this.Match.Value; }
\r
800 /// 指定したパターンを置き換えて返す
\r
802 /// <param name="replacement">置き換える文字列</param>
\r
803 /// <returns>置き換え後の文字列</returns>
\r
804 public string Result(string replacement)
\r
806 return this.Match.Result(replacement);
\r
812 /// <param name="m">Matchオブジェクト</param>
\r
813 /// <param name="start">開始インデックス</param>
\r
814 /// <param name="end">終了インデックス</param>
\r
815 public SearchResult(Match m, int start,int end)
\r
818 this.Start = start;
\r
826 public class DocumentReader : TextReader
\r
828 StringBuffer document;
\r
834 /// <param name="doc"></param>
\r
835 internal DocumentReader(StringBuffer doc)
\r
838 throw new ArgumentNullException();
\r
839 this.document = doc;
\r
845 /// <returns>文字。取得できない場合は-1</returns>
\r
846 public override int Peek()
\r
848 if (this.document == null)
\r
849 throw new InvalidOperationException();
\r
850 if (this.currentIndex >= this.document.Length)
\r
852 return this.document[this.currentIndex];
\r
856 /// 文字を取得し、イテレーターを一つ進める
\r
858 /// <returns>文字。取得できない場合は-1</returns>
\r
859 public override int Read()
\r
861 int c = this.Peek();
\r
863 this.currentIndex++;
\r
868 /// 文字列を読み取りバッファーに書き込む
\r
870 /// <param name="buffer">バッファー</param>
\r
871 /// <param name="index">開始インデックス</param>
\r
872 /// <param name="count">カウント</param>
\r
873 /// <returns>読み取られた文字数</returns>
\r
874 public override int Read(char[] buffer, int index, int count)
\r
876 if (this.document == null)
\r
877 throw new InvalidOperationException();
\r
879 if (buffer == null)
\r
880 throw new ArgumentNullException();
\r
882 if (this.document.Length < count)
\r
883 throw new ArgumentException();
\r
885 if (index < 0 || count < 0)
\r
886 throw new ArgumentOutOfRangeException();
\r
888 if (this.document.Length == 0)
\r
891 int actualCount = count;
\r
892 if (index + count - 1 > this.document.Length - 1)
\r
893 actualCount = this.document.Length - index;
\r
895 string str = this.document.ToString(index, actualCount);
\r
897 for (int i = 0; i < str.Length; i++) //ToCharArray()だと戻った時に消えてしまう
\r
898 buffer[i] = str[i];
\r
900 this.currentIndex = index + actualCount;
\r
902 return actualCount;
\r
908 /// <param name="disposing">真ならアンマネージドリソースを解放する</param>
\r
909 protected override void Dispose(bool disposing)
\r
911 this.document = null;
\r