2 * Copyright (C) 2013 FooProject
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
4 * the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
6 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
7 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
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/>.
16 using System.Collections.Generic;
18 using System.Text.RegularExpressions;
19 using System.Threading;
20 using System.Threading.Tasks;
23 namespace FooEditEngine
28 public enum ProgressState
42 public sealed class ProgressEventArgs : EventArgs
47 public ProgressState state;
51 /// <param name="state">ProgressStateオブジェクト</param>
52 public ProgressEventArgs(ProgressState state)
61 /// <param name="sender">送信元クラス</param>
62 /// <param name="e">イベントデータ</param>
63 public delegate void ProgressEventHandler(object sender, ProgressEventArgs e);
68 public enum UpdateType
71 /// ドキュメントが置き換えられたことを表す
75 /// ドキュメント全体が削除されたことを表す
81 /// 更新タイプを通知するためのイベントデータ
83 public sealed class DocumentUpdateEventArgs : EventArgs
88 public UpdateType type;
92 public int startIndex;
96 public int removeLength;
100 public int insertLength;
102 /// 更新イベントが発生した行。行が不明な場合や行をまたぐ場合はnullを指定すること。
108 /// <param name="type">更新タイプ</param>
109 /// <param name="startIndex">開始インデックス</param>
110 /// <param name="removeLength">削除された長さ</param>
111 /// <param name="insertLength">追加された長さ</param>
112 /// <param name="row">開始行。nullを指定することができる</param>
113 public DocumentUpdateEventArgs(UpdateType type, int startIndex, int removeLength, int insertLength, int? row = null)
116 this.startIndex = startIndex;
117 this.removeLength = removeLength;
118 this.insertLength = insertLength;
124 /// ドキュメントに更新があったことを伝えるためのデリゲート
126 /// <param name="sender">送信元クラス</param>
127 /// <param name="e">イベントデータ</param>
128 public delegate void DocumentUpdateEventHandler(object sender, DocumentUpdateEventArgs e);
133 /// <remarks>この型のすべてのメソッド・プロパティはスレッドセーフです</remarks>
134 public sealed class Document : IEnumerable<char>, IRandomEnumrator<char>
136 const int MaxSemaphoreCount = 1;
140 LineToIndexTable _LayoutLines;
141 bool _EnableFireUpdateEvent = true,_UrlMark = false, _DrawLineNumber = false, _HideRuler = true, _RightToLeft = false;
142 SemaphoreSlim Semaphore = new SemaphoreSlim(MaxSemaphoreCount);
143 LineBreakMethod _LineBreak;
144 int _TabStops, _LineBreakCharCount = 80;
149 public const int MaximumLineLength = 1000;
159 internal Document(Document doc)
162 this.buffer = new StringBuffer();
164 this.buffer = new StringBuffer(doc.buffer);
165 this.buffer.Update = new DocumentUpdateEventHandler(buffer_Update);
166 this.UpdateCalledAlways += (s, e) => { };
167 this.Update += new DocumentUpdateEventHandler((s, e) => { });
168 this.ChangeFireUpdateEvent += new EventHandler((s, e) => { });
169 this.Markers = new MarkerCollection(this);
170 this.UndoManager = new UndoManager();
171 this._LayoutLines = new LineToIndexTable(this);
172 this._LayoutLines.SpilitString = (s,e)=> {
173 return this.CreateLineList(e.index, e.length, MaximumLineLength);
175 this._LayoutLines.Clear();
176 this.MarkerPatternSet = new MarkerPatternSet(this._LayoutLines, this.Markers);
177 this.MarkerPatternSet.Updated += WacthDogPattern_Updated;
178 this.LineBreakChanged += (s, e) => { };
179 this.TabStopsChanged += (s, e) => { };
180 this.DrawLineNumberChanged += (s, e) => { };
181 this.Selections = new SelectCollection();
182 this.HideLineMarker = true;
183 this.HideRulerChanged += (s, e) => { };
184 this.CaretPostion = TextPoint.Null;
187 void WacthDogPattern_Updated(object sender, EventArgs e)
189 this._LayoutLines.ClearLayoutCache();
193 /// HideRulerの値が変わったときに通知します
195 public event EventHandler RightToLeftChanged;
200 public bool RightToLeft {
201 get { return this._RightToLeft; }
204 this._RightToLeft = value;
205 this.RightToLeftChanged(this, null);
210 /// 矩形選択モードなら真を返し、そうでない場合は偽を返す
212 public bool RectSelection
221 public IndentMode IndentMode
228 /// ラインマーカーを描くなら偽。そうでなければ真
230 public bool HideLineMarker
237 /// キャレットを描くなら偽。そうでなければ真
239 public bool HideCaret
246 /// 挿入モードなら真を返し、上書きモードなら偽を返す
248 public bool InsertMode
255 /// HideRulerの値が変わったときに通知します
257 public event EventHandler HideRulerChanged;
260 /// ルーラーを表示しないなら真、そうでないなら偽
262 public bool HideRuler
264 get { return this._HideRuler; }
267 this._HideRuler = value;
268 this.LayoutLines.ClearLayoutCache();
269 this.HideRulerChanged(this, null);
274 /// レイアウト行のどこにキャレットがあるかを表す
276 /// <remarks>この値を変更しても反映されないので、EditView側でAdjustCaret()メソッドを呼び出す必要があります</remarks>
277 public TextPoint CaretPostion
286 internal SelectCollection Selections
293 /// DrawLineNumberの値が変わったときに通知される
295 public event EventHandler DrawLineNumberChanged;
300 public bool DrawLineNumber
302 get { return this._DrawLineNumber; }
305 this._DrawLineNumber = value;
306 this._LayoutLines.ClearLayoutCache();
307 this.DrawLineNumberChanged(this, null);
312 /// URLをハイパーリンクとして表示するなら真。そうでないなら偽
316 get { return this._UrlMark; }
319 this._UrlMark = value;
322 Regex regex = new Regex("(http|https|ftp)(:\\/\\/[-_.!~*\\'()a-zA-Z0-9;\\/?:\\@&=+\\$,%#]+)");
323 this.MarkerPatternSet.Add(MarkerIDs.URL, new RegexMarkerPattern(regex, HilightType.Url, new Color()));
327 this.MarkerPatternSet.Remove(MarkerIDs.URL);
333 /// LineBreakが変更されたときに通知される
335 public EventHandler LineBreakChanged;
341 /// 変更した場合、呼び出し側で再描写とレイアウトの再構築を行う必要があります
343 public LineBreakMethod LineBreak
347 return this._LineBreak;
351 this._LineBreak = value;
352 this.LineBreakChanged(this, null);
357 /// 折り返し行う文字数。実際に折り返しが行われる幅はem単位×この値となります
359 /// <remarks>この値を変えた場合、LineBreakChangedイベントが発生します</remarks>
360 public int LineBreakCharCount
364 return this._LineBreakCharCount;
368 this._LineBreakCharCount = value;
369 this.LineBreakChanged(this, null);
374 /// TabStopsの値が変わったことを通知する
376 public event EventHandler TabStopsChanged;
381 /// <remarks>変更した場合、呼び出し側で再描写する必要があります</remarks>
384 get { return this._TabStops; }
386 this._TabStops = value;
387 this.TabStopsChanged(this, null);
394 public MarkerPatternSet MarkerPatternSet
403 public LineToIndexTable LayoutLines
407 return this._LayoutLines;
414 /// <param name="index">開始インデックス</param>
415 /// <param name="length">長さ</param>
416 /// <param name="lineLimitLength">1行当たりの最大文字数。-1で無制限</param>
417 /// <returns>レイアウト行リスト</returns>
418 internal IList<LineToIndexTableData> CreateLineList(int index, int length, int lineLimitLength = -1)
420 int startIndex = index;
421 int endIndex = index + length - 1;
422 List<LineToIndexTableData> output = new List<LineToIndexTableData>();
424 foreach (Tuple<int, int> range in this.ForEachLines(startIndex, endIndex, lineLimitLength))
426 int lineHeadIndex = range.Item1;
427 int lineLength = range.Item2;
428 char c = this.buffer[lineHeadIndex + lineLength - 1];
429 bool hasNewLine = c == Document.NewLine;
430 output.Add(this.LayoutLines.CreateLineToIndexTableData(lineHeadIndex, lineLength, hasNewLine, null));
433 if (output.Count > 0)
434 output.Last().LineEnd = true;
439 internal void FireUpdate(DocumentUpdateEventArgs e)
441 this.buffer_Update(this.buffer, e);
445 /// ドキュメントが更新された時に呼ばれるイベント
447 public event DocumentUpdateEventHandler Update;
450 /// ドキュメントが更新された時に呼びされるイベント
453 /// FireUpdateEventの値に関わらず常に呼びされます
455 internal event DocumentUpdateEventHandler UpdateCalledAlways;
458 /// FireUpdateEventの値が変わったときに呼び出されるイベント
460 public event EventHandler ChangeFireUpdateEvent;
465 public const char NewLine = '\n';
470 public const char EndOfFile = '\u001a';
473 /// ロック中なら真を返し、そうでないなら偽を返す
479 return this.Semaphore.CurrentCount == 0;
486 public UndoManager UndoManager
499 return this.buffer.Length;
504 /// 変更のたびにUpdateイベントを発生させるかどうか
506 public bool FireUpdateEvent
510 return this._EnableFireUpdateEvent;
514 this._EnableFireUpdateEvent = value;
515 this.ChangeFireUpdateEvent(this, null);
522 /// <param name="i">インデックス(自然数でなければならない)</param>
523 /// <returns>Char型</returns>
524 public char this[int i]
528 return this.buffer[i];
535 public MarkerCollection Markers
541 internal StringBuffer StringBuffer
550 /// DocumentReaderを作成します
552 /// <returns>DocumentReaderオブジェクト</returns>
553 public DocumentReader CreateReader()
555 return new DocumentReader(this.buffer);
563 this.Semaphore.Release();
571 this.Semaphore.Wait();
577 /// <returns>Taskオブジェクト</returns>
578 public Task LockAsync()
580 return this.Semaphore.WaitAsync();
586 /// <param name="id">マーカーID</param>
587 /// <param name="m">設定したいマーカー</param>
588 public void SetMarker(int id,Marker m)
590 if (m.start < 0 || m.start + m.length > this.Length)
591 throw new ArgumentOutOfRangeException("startもしくはendが指定できる範囲を超えています");
593 this.Markers.Add(id,m);
599 /// <param name="id">マーカーID</param>
600 /// <param name="start">開始インデックス</param>
601 /// <param name="length">削除する長さ</param>
602 public void RemoveMarker(int id,int start, int length)
604 if (start < 0 || start + length > this.Length)
605 throw new ArgumentOutOfRangeException("startもしくはendが指定できる範囲を超えています");
607 this.Markers.RemoveAll(id,start, length);
613 /// <param name="id">マーカーID</param>
614 /// <param name="type">削除したいマーカーのタイプ</param>
615 public void RemoveMarker(int id, HilightType type)
617 this.Markers.RemoveAll(id,type);
621 /// インデックスに対応するマーカーを得る
623 /// <param name="id">マーカーID</param>
624 /// <param name="index">インデックス</param>
625 /// <returns>Marker構造体の列挙子</returns>
626 public IEnumerable<Marker> GetMarkers(int id, int index)
628 if (index < 0 || index > this.Length)
629 throw new ArgumentOutOfRangeException("indexが範囲を超えています");
630 return this.Markers.Get(id,index);
636 /// <param name="index">開始インデックス</param>
637 /// <param name="length">長さ</param>
638 /// <returns>Stringオブジェクト</returns>
639 public string ToString(int index, int length)
641 return this.buffer.ToString(index, length);
645 /// インデックスを開始位置とする文字列を返す
647 /// <param name="index">開始インデックス</param>
648 /// <returns>Stringオブジェクト</returns>
649 public string ToString(int index)
651 return this.ToString(index, this.buffer.Length - index);
657 /// <param name="startIndex">開始インデックス</param>
658 /// <param name="endIndex">終了インデックス</param>
659 /// <param name="maxCharCount">最大長</param>
660 /// <returns>行イテレーターが返される</returns>
661 public IEnumerable<string> GetLines(int startIndex, int endIndex, int maxCharCount = -1)
663 return this.buffer.GetLines(startIndex, endIndex, maxCharCount);
666 internal IEnumerable<Tuple<int, int>> ForEachLines(int startIndex, int endIndex, int maxCharCount = -1)
668 return this.buffer.ForEachLines(startIndex, endIndex, maxCharCount);
675 /// <param name="s">追加したい文字列</param>
676 /// <remarks>非同期操作中はこのメソッドを実行することはできません</remarks>
677 public void Append(string s)
679 this.Replace(this.buffer.Length, 0, s);
685 /// <param name="index">開始インデックス</param>
686 /// <param name="s">追加したい文字列</param>
687 /// <remarks>読み出し操作中はこのメソッドを実行することはできません</remarks>
688 public void Insert(int index, string s)
690 this.Replace(index, 0, s);
696 /// <param name="index">開始インデックス</param>
697 /// <param name="length">長さ</param>
698 /// <remarks>読み出し操作中はこのメソッドを実行することはできません</remarks>
699 public void Remove(int index, int length)
701 this.Replace(index, length, "");
707 /// <param name="index">開始インデックス</param>
708 /// <param name="length">長さ</param>
709 /// <param name="s">文字列</param>
710 /// <remarks>読み出し操作中はこのメソッドを実行することはできません</remarks>
711 public void Replace(int index, int length, string s)
713 if (index < 0 || index > this.buffer.Length || index + length > this.buffer.Length || length < 0)
714 throw new ArgumentOutOfRangeException();
715 if (length == 0 && (s == string.Empty || s == null))
718 foreach(int id in this.Markers.IDs)
719 this.RemoveMarker(id,index, length);
721 ReplaceCommand cmd = new ReplaceCommand(this.buffer, index, length, s);
722 this.UndoManager.push(cmd);
729 /// <remarks>Dirtyフラグも同時にクリアーされます</remarks>
730 /// <remarks>非同期操作中はこのメソッドを実行することはできません</remarks>
737 /// ストリームからドキュメントを非同期的に構築します
739 /// <param name="fs">IStreamReaderオブジェクト</param>
740 /// <param name="tokenSource">キャンセルトークン</param>
741 /// <returns>Taskオブジェクト</returns>
743 /// 読み取り操作は別スレッドで行われます。
744 /// また、非同期操作中はこのメソッドを実行することはできません。
746 internal async Task LoadAsync(IStreamReader fs, CancellationTokenSource tokenSource = null)
753 await this.LockAsync().ConfigureAwait(false);
755 this.FireUpdateEvent = false;
756 await this.buffer.LoadAsync(fs, tokenSource);
760 this.FireUpdateEvent = true;
766 /// ストリームに非同期モードで保存します
768 /// <param name="fs">IStreamWriterオブジェクト</param>
769 /// <param name="tokenSource">キャンセルトークン</param>
770 /// <returns>Taskオブジェクト</returns>
771 /// <remarks>非同期操作中はこのメソッドを実行することはできません</remarks>
772 internal async Task SaveAsync(IStreamWriter fs, CancellationTokenSource tokenSource = null)
776 await this.LockAsync().ConfigureAwait(false);
777 StringBuilder line = new StringBuilder();
778 for (int i = 0; i < this.Length; i++)
782 if (c == Document.NewLine || i == this.Length - 1)
784 string str = line.ToString();
785 str = str.Replace(Document.NewLine.ToString(), fs.NewLine);
786 await fs.WriteAsync(str).ConfigureAwait(false);
788 if (tokenSource != null)
789 tokenSource.Token.ThrowIfCancellationRequested();
791 System.Threading.Thread.Sleep(10);
803 /// Find()およびReplaceAll()で使用するパラメーターをセットします
805 /// <param name="pattern">検索したい文字列</param>
806 /// <param name="UseRegex">正規表現を使用するなら真</param>
807 /// <param name="opt">RegexOptions列挙体</param>
808 public void SetFindParam(string pattern, bool UseRegex, RegexOptions opt)
812 this.regex = new Regex(pattern, opt);
814 this.regex = new Regex(Regex.Escape(pattern), opt);
818 /// 現在の検索パラメーターでWatchDogを生成する
820 /// <param name="type">ハイライトタイプ</param>
821 /// <param name="color">色</param>
822 /// <returns>WatchDogオブジェクト</returns>
823 public RegexMarkerPattern CreateWatchDogByFindParam(HilightType type,Color color)
825 if (this.regex == null)
826 throw new InvalidOperationException("SetFindParam()を呼び出してください");
827 return new RegexMarkerPattern(this.regex,type,color);
833 /// <returns>見つかった場合はSearchResult列挙子を返却します</returns>
834 /// <remarks>見つかったパターン以外を置き換えた場合、正常に動作しないことがあります</remarks>
835 public IEnumerator<SearchResult> Find()
837 return this.Find(0, this.Length);
843 /// <returns>見つかった場合はSearchResult列挙子を返却します</returns>
844 /// <param name="start">開始インデックス</param>
845 /// <param name="length">検索する長さ</param>
846 /// <remarks>見つかったパターン以外を置き換えた場合、正常に動作しないことがあります</remarks>
847 public IEnumerator<SearchResult> Find(int start, int length)
849 if (this.regex == null)
850 throw new InvalidOperationException();
851 if (start < 0 || start >= this.Length)
852 throw new ArgumentOutOfRangeException();
854 int end = start + length - 1;
856 if(end > this.Length - 1)
857 throw new ArgumentOutOfRangeException();
859 StringBuilder line = new StringBuilder();
860 int oldLength = this.Length;
861 for (int i = start; i <= end; i++)
865 if (c == Document.NewLine || i == end)
867 this.match = this.regex.Match(line.ToString());
868 while (this.match.Success)
870 int startIndex = i - line.Length + 1 + this.match.Index;
871 int endIndex = startIndex + this.match.Length - 1;
873 yield return new SearchResult(this.match, startIndex, endIndex);
875 if (this.Length != oldLength) //長さが変わった場合は置き換え後のパターンの終点+1まで戻る
877 int delta = this.Length - oldLength;
878 i = endIndex + delta;
880 oldLength = this.Length;
884 this.match = this.match.NextMatch();
892 /// 任意のパターンですべて置き換えます
894 /// <param name="replacePattern">置き換え後のパターン</param>
895 /// <param name="groupReplace">グループ置き換えを行うなら真。そうでないなら偽</param>
896 public void ReplaceAll(string replacePattern,bool groupReplace)
898 if (this.regex == null)
899 throw new InvalidOperationException();
900 ReplaceAllCommand cmd = new ReplaceAllCommand(this.buffer, this.LayoutLines, this.regex, replacePattern, groupReplace);
901 this.UndoManager.push(cmd);
908 /// <param name="target">対象となる文字列</param>
909 /// <param name="pattern">置き換え後の文字列</param>
910 /// <param name="ci">大文字も文字を区別しないなら真。そうでないなら偽</param>
912 /// 検索時に大文字小文字を区別します。また、このメソッドでは正規表現を使用することはできません
914 public void ReplaceAll2(string target, string pattern,bool ci = false)
916 FastReplaceAllCommand cmd = new FastReplaceAllCommand(this.buffer, this.LayoutLines, target, pattern,ci);
917 this.UndoManager.push(cmd);
921 #region IEnumerable<char> メンバー
926 /// <returns>IEnumeratorオブジェクトを返す</returns>
927 public IEnumerator<char> GetEnumerator()
929 return this.buffer.GetEnumerator();
934 #region IEnumerable メンバー
936 System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
938 throw new NotImplementedException();
943 void buffer_Update(object sender, DocumentUpdateEventArgs e)
947 case UpdateType.Replace:
949 this._LayoutLines.UpdateAsReplace(e.startIndex, e.removeLength, e.insertLength);
951 this._LayoutLines.UpdateLineAsReplace(e.row.Value, e.removeLength, e.insertLength);
953 case UpdateType.Clear:
954 this._LayoutLines.Clear();
957 this.UpdateCalledAlways(this, e);
958 if(this.FireUpdateEvent)
959 this.Update(this, e);
963 public interface IStreamReader
971 /// ストリームから行を読み取った物を返す。LoadAsyncを呼び出す場合は必ず実装してください
973 Task<string> ReadLineAsync();
975 /// ストリームから指定した文字数だけ読み取る
977 /// <param name="buffer">書き込み先バッファー</param>
978 /// <param name="index">書き込み先バッファーのインデックス</param>
979 /// <param name="count">読み取る文字数</param>
980 /// <returns>読み取った文字数</returns>
981 Task<int> ReadAsync(char[] buffer, int index, int count);
984 public interface IStreamWriter
987 /// ストリームに書き込む。SaveAsyncを呼び出す場合は必ず実装してください
989 Task WriteAsync(string str);
1004 public class SearchResult
1006 private Match Match;
1023 get { return this.Match.Value; }
1027 /// 指定したパターンを置き換えて返す
1029 /// <param name="replacement">置き換える文字列</param>
1030 /// <returns>置き換え後の文字列</returns>
1031 public string Result(string replacement)
1033 return this.Match.Result(replacement);
1039 /// <param name="m">Matchオブジェクト</param>
1040 /// <param name="start">開始インデックス</param>
1041 /// <param name="end">終了インデックス</param>
1042 public SearchResult(Match m, int start,int end)
1053 public class DocumentReader : TextReader
1055 StringBuffer document;
1061 /// <param name="doc"></param>
1062 internal DocumentReader(StringBuffer doc)
1065 throw new ArgumentNullException();
1066 this.document = doc;
1072 /// <returns>文字。取得できない場合は-1</returns>
1073 public override int Peek()
1075 if (this.document == null)
1076 throw new InvalidOperationException();
1077 if (this.currentIndex >= this.document.Length)
1079 return this.document[this.currentIndex];
1083 /// 文字を取得し、イテレーターを一つ進める
1085 /// <returns>文字。取得できない場合は-1</returns>
1086 public override int Read()
1088 int c = this.Peek();
1090 this.currentIndex++;
1095 /// 文字列を読み取りバッファーに書き込む
1097 /// <param name="buffer">バッファー</param>
1098 /// <param name="index">開始インデックス</param>
1099 /// <param name="count">カウント</param>
1100 /// <returns>読み取られた文字数</returns>
1101 public override int Read(char[] buffer, int index, int count)
1103 if (this.document == null)
1104 throw new InvalidOperationException();
1107 throw new ArgumentNullException();
1109 if (this.document.Length < count)
1110 throw new ArgumentException();
1112 if (index < 0 || count < 0)
1113 throw new ArgumentOutOfRangeException();
1115 if (this.document.Length == 0)
1118 int actualCount = count;
1119 if (index + count - 1 > this.document.Length - 1)
1120 actualCount = this.document.Length - index;
1122 string str = this.document.ToString(index, actualCount);
1124 for (int i = 0; i < str.Length; i++) //ToCharArray()だと戻った時に消えてしまう
1127 this.currentIndex = index + actualCount;
1135 /// <param name="disposing">真ならアンマネージドリソースを解放する</param>
1136 protected override void Dispose(bool disposing)
1138 this.document = null;