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 const int EmptyValue = -1;
92 public UpdateType type;
96 public int startIndex;
100 public int removeLength;
104 public int insertLength;
106 /// 更新イベントが発生した行。行が不明な場合や行をまたぐ場合はnullを指定すること。
112 /// <param name="type">更新タイプ</param>
113 /// <param name="startIndex">開始インデックス</param>
114 /// <param name="removeLength">削除された長さ</param>
115 /// <param name="insertLength">追加された長さ</param>
116 /// <param name="row">開始行。nullを指定することができる</param>
117 public DocumentUpdateEventArgs(UpdateType type, int startIndex = EmptyValue, int removeLength = EmptyValue, int insertLength = EmptyValue, int? row = null)
120 this.startIndex = startIndex;
121 this.removeLength = removeLength;
122 this.insertLength = insertLength;
128 /// ドキュメントに更新があったことを伝えるためのデリゲート
130 /// <param name="sender">送信元クラス</param>
131 /// <param name="e">イベントデータ</param>
132 public delegate void DocumentUpdateEventHandler(object sender, DocumentUpdateEventArgs e);
137 /// <remarks>この型のすべてのメソッド・プロパティはスレッドセーフです</remarks>
138 public sealed class Document : IEnumerable<char>, IRandomEnumrator<char>
140 const int MaxSemaphoreCount = 1;
144 LineToIndexTable _LayoutLines;
145 bool _EnableFireUpdateEvent = true,_UrlMark = false, _DrawLineNumber = false, _HideRuler = true, _RightToLeft = false;
146 SemaphoreSlim Semaphore = new SemaphoreSlim(MaxSemaphoreCount);
147 LineBreakMethod _LineBreak;
148 int _TabStops, _LineBreakCharCount = 80;
149 bool _ShowFullSpace, _ShowHalfSpace, _ShowTab, _ShowLineBreak;
154 public const int MaximumLineLength = 1000;
167 /// <param name="doc">ドキュメントオブジェクト</param>
168 /// <remarks>docが複製されますが、プロパティは引き継がれません</remarks>
169 public Document(Document doc)
172 this.buffer = new StringBuffer();
174 this.buffer = new StringBuffer(doc.buffer);
175 this.buffer.Update = new DocumentUpdateEventHandler(buffer_Update);
176 this.UpdateCalledAlways += (s, e) => { };
177 this.Update += new DocumentUpdateEventHandler((s, e) => { });
178 this.ChangeFireUpdateEvent += new EventHandler((s, e) => { });
179 this.Markers = new MarkerCollection();
180 this.UndoManager = new UndoManager();
181 this._LayoutLines = new LineToIndexTable(this);
182 this._LayoutLines.SpilitString = (s,e)=> {
183 return this.CreateLineList(e.index, e.length, MaximumLineLength);
185 this._LayoutLines.Clear();
186 this.MarkerPatternSet = new MarkerPatternSet(this._LayoutLines, this.Markers);
187 this.MarkerPatternSet.Updated += WacthDogPattern_Updated;
188 this.Selections = new SelectCollection();
189 this.CaretPostion = new TextPoint();
190 this.HideLineMarker = true;
191 this.SelectGrippers = new GripperRectangle(new Gripper(), new Gripper());
194 void WacthDogPattern_Updated(object sender, EventArgs e)
196 this._LayoutLines.ClearLayoutCache();
200 /// キャレットでの選択の起点となる位置
202 public int AnchorIndex
218 /// ルーラーやキャレット・行番号などの表示すべきものが変化した場合に呼び出される。ドキュメントの内容が変化した通知を受け取り場合はUpdateを使用してください
220 public event EventHandler StatusUpdate;
225 public bool ShowFullSpace
227 get { return this._ShowFullSpace; }
230 this._ShowFullSpace = value;
231 this.StatusUpdate(this, null);
238 public bool ShowHalfSpace
240 get { return this._ShowHalfSpace; }
243 this._ShowHalfSpace = value;
244 this.StatusUpdate(this, null);
253 get { return this._ShowTab; }
256 this._ShowTab = value;
257 this.StatusUpdate(this, null);
264 public bool ShowLineBreak
266 get { return this._ShowLineBreak; }
269 this._ShowLineBreak = value;
270 this.StatusUpdate(this, null);
277 internal GripperRectangle SelectGrippers
286 public bool RightToLeft {
287 get { return this._RightToLeft; }
290 this._RightToLeft = value;
291 this.StatusUpdate(this, null);
296 /// 矩形選択モードなら真を返し、そうでない場合は偽を返す
298 public bool RectSelection
307 public IndentMode IndentMode
314 /// ラインマーカーを描くなら偽。そうでなければ真
316 public bool HideLineMarker
323 /// キャレットを描くなら偽。そうでなければ真
325 public bool HideCaret
332 /// 挿入モードなら真を返し、上書きモードなら偽を返す
334 public bool InsertMode
341 /// ルーラーを表示しないなら真、そうでないなら偽
343 public bool HideRuler
345 get { return this._HideRuler; }
348 this._HideRuler = value;
349 this.LayoutLines.ClearLayoutCache();
350 this.StatusUpdate(this, null);
355 /// レイアウト行のどこにキャレットがあるかを表す
357 /// <remarks>この値を変更しても反映されないので、EditView側でAdjustCaret()メソッドを呼び出す必要があります</remarks>
358 public TextPoint CaretPostion
367 internal SelectCollection Selections
376 public bool DrawLineNumber
378 get { return this._DrawLineNumber; }
381 this._DrawLineNumber = value;
382 this._LayoutLines.ClearLayoutCache();
383 this.StatusUpdate(this, null);
388 /// URLをハイパーリンクとして表示するなら真。そうでないなら偽
392 get { return this._UrlMark; }
395 this._UrlMark = value;
398 Regex regex = new Regex("(http|https|ftp)(:\\/\\/[-_.!~*\\'()a-zA-Z0-9;\\/?:\\@&=+\\$,%#]+)");
399 this.MarkerPatternSet.Add(MarkerIDs.URL, new RegexMarkerPattern(regex, HilightType.Url, new Color()));
403 this.MarkerPatternSet.Remove(MarkerIDs.URL);
408 public event EventHandler LineBreakChanged;
414 /// 変更した場合、呼び出し側で再描写とレイアウトの再構築を行う必要があります
415 /// また、StatusUpdatedではなく、LineBreakChangedイベントが発生します
417 public LineBreakMethod LineBreak
421 return this._LineBreak;
425 this._LineBreak = value;
426 this.LineBreakChanged(this, null);
431 /// 折り返し行う文字数。実際に折り返しが行われる幅はem単位×この値となります
433 /// <remarks>この値を変えた場合、LineBreakChangedイベントが発生します</remarks>
434 public int LineBreakCharCount
438 return this._LineBreakCharCount;
442 this._LineBreakCharCount = value;
443 this.LineBreakChanged(this, null);
450 /// <remarks>変更した場合、呼び出し側で再描写する必要があります</remarks>
453 get { return this._TabStops; }
455 this._TabStops = value;
456 this.StatusUpdate(this, null);
463 public MarkerPatternSet MarkerPatternSet
472 public LineToIndexTable LayoutLines
476 return this._LayoutLines;
483 /// <param name="index">開始インデックス</param>
484 /// <param name="length">長さ</param>
485 /// <param name="lineLimitLength">1行当たりの最大文字数。-1で無制限</param>
486 /// <returns>レイアウト行リスト</returns>
487 internal IList<LineToIndexTableData> CreateLineList(int index, int length, int lineLimitLength = -1)
489 int startIndex = index;
490 int endIndex = index + length - 1;
491 List<LineToIndexTableData> output = new List<LineToIndexTableData>();
493 foreach (Tuple<int, int> range in this.ForEachLines(startIndex, endIndex, lineLimitLength))
495 int lineHeadIndex = range.Item1;
496 int lineLength = range.Item2;
497 char c = this.buffer[lineHeadIndex + lineLength - 1];
498 bool hasNewLine = c == Document.NewLine;
499 output.Add(this.LayoutLines.CreateLineToIndexTableData(lineHeadIndex, lineLength, hasNewLine, null));
502 if (output.Count > 0)
503 output.Last().LineEnd = true;
508 internal void FireUpdate(DocumentUpdateEventArgs e)
510 this.buffer_Update(this.buffer, e);
514 /// ドキュメントが更新された時に呼ばれるイベント
516 public event DocumentUpdateEventHandler Update;
519 /// ドキュメントが更新された時に呼びされるイベント
522 /// FireUpdateEventの値に関わらず常に呼びされます
524 internal event DocumentUpdateEventHandler UpdateCalledAlways;
527 /// FireUpdateEventの値が変わったときに呼び出されるイベント
529 public event EventHandler ChangeFireUpdateEvent;
534 public const char NewLine = '\n';
539 public const char EndOfFile = '\u001a';
542 /// ロック中なら真を返し、そうでないなら偽を返す
548 return this.Semaphore.CurrentCount == 0;
555 public UndoManager UndoManager
568 return this.buffer.Length;
573 /// 変更のたびにUpdateイベントを発生させるかどうか
575 public bool FireUpdateEvent
579 return this._EnableFireUpdateEvent;
583 this._EnableFireUpdateEvent = value;
584 this.ChangeFireUpdateEvent(this, null);
591 /// <param name="i">インデックス(自然数でなければならない)</param>
592 /// <returns>Char型</returns>
593 public char this[int i]
597 return this.buffer[i];
604 public MarkerCollection Markers
610 internal StringBuffer StringBuffer
619 /// DocumentReaderを作成します
621 /// <returns>DocumentReaderオブジェクト</returns>
622 public DocumentReader CreateReader()
624 return new DocumentReader(this.buffer);
632 this.Semaphore.Release();
640 this.Semaphore.Wait();
646 /// <returns>Taskオブジェクト</returns>
647 public Task LockAsync()
649 return this.Semaphore.WaitAsync();
655 /// <param name="id">マーカーID</param>
656 /// <param name="m">設定したいマーカー</param>
657 public void SetMarker(int id,Marker m)
659 if (m.start < 0 || m.start + m.length > this.Length)
660 throw new ArgumentOutOfRangeException("startもしくはendが指定できる範囲を超えています");
662 this.Markers.Add(id,m);
668 /// <param name="id">マーカーID</param>
669 /// <param name="start">開始インデックス</param>
670 /// <param name="length">削除する長さ</param>
671 public void RemoveMarker(int id,int start, int length)
673 if (start < 0 || start + length > this.Length)
674 throw new ArgumentOutOfRangeException("startもしくはendが指定できる範囲を超えています");
676 this.Markers.RemoveAll(id,start, length);
682 /// <param name="id">マーカーID</param>
683 /// <param name="type">削除したいマーカーのタイプ</param>
684 public void RemoveMarker(int id, HilightType type)
686 this.Markers.RemoveAll(id,type);
690 /// インデックスに対応するマーカーを得る
692 /// <param name="id">マーカーID</param>
693 /// <param name="index">インデックス</param>
694 /// <returns>Marker構造体の列挙子</returns>
695 public IEnumerable<Marker> GetMarkers(int id, int index)
697 if (index < 0 || index > this.Length)
698 throw new ArgumentOutOfRangeException("indexが範囲を超えています");
699 return this.Markers.Get(id,index);
705 /// <param name="index">開始インデックス</param>
706 /// <param name="length">長さ</param>
707 /// <returns>Stringオブジェクト</returns>
708 public string ToString(int index, int length)
710 return this.buffer.ToString(index, length);
714 /// インデックスを開始位置とする文字列を返す
716 /// <param name="index">開始インデックス</param>
717 /// <returns>Stringオブジェクト</returns>
718 public string ToString(int index)
720 return this.ToString(index, this.buffer.Length - index);
726 /// <param name="startIndex">開始インデックス</param>
727 /// <param name="endIndex">終了インデックス</param>
728 /// <param name="maxCharCount">最大長</param>
729 /// <returns>行イテレーターが返される</returns>
730 public IEnumerable<string> GetLines(int startIndex, int endIndex, int maxCharCount = -1)
732 return this.buffer.GetLines(startIndex, endIndex, maxCharCount);
735 internal IEnumerable<Tuple<int, int>> ForEachLines(int startIndex, int endIndex, int maxCharCount = -1)
737 return this.buffer.ForEachLines(startIndex, endIndex, maxCharCount);
744 /// <param name="s">追加したい文字列</param>
745 /// <remarks>非同期操作中はこのメソッドを実行することはできません</remarks>
746 public void Append(string s)
748 this.Replace(this.buffer.Length, 0, s);
754 /// <param name="index">開始インデックス</param>
755 /// <param name="s">追加したい文字列</param>
756 /// <remarks>読み出し操作中はこのメソッドを実行することはできません</remarks>
757 public void Insert(int index, string s)
759 this.Replace(index, 0, s);
765 /// <param name="index">開始インデックス</param>
766 /// <param name="length">長さ</param>
767 /// <remarks>読み出し操作中はこのメソッドを実行することはできません</remarks>
768 public void Remove(int index, int length)
770 this.Replace(index, length, "");
776 /// <param name="index">開始インデックス</param>
777 /// <param name="length">長さ</param>
778 /// <param name="s">文字列</param>
779 /// <remarks>読み出し操作中はこのメソッドを実行することはできません</remarks>
780 public void Replace(int index, int length, string s)
782 if (index < 0 || index > this.buffer.Length || index + length > this.buffer.Length || length < 0)
783 throw new ArgumentOutOfRangeException();
784 if (length == 0 && (s == string.Empty || s == null))
787 foreach(int id in this.Markers.IDs)
788 this.RemoveMarker(id,index, length);
790 ReplaceCommand cmd = new ReplaceCommand(this.buffer, index, length, s);
791 this.UndoManager.push(cmd);
798 /// <remarks>Dirtyフラグも同時にクリアーされます</remarks>
799 /// <remarks>非同期操作中はこのメソッドを実行することはできません</remarks>
806 /// ストリームからドキュメントを非同期的に構築します
808 /// <param name="fs">IStreamReaderオブジェクト</param>
809 /// <param name="tokenSource">キャンセルトークン</param>
810 /// <returns>Taskオブジェクト</returns>
812 /// 読み取り操作は別スレッドで行われます。
813 /// また、非同期操作中はこのメソッドを実行することはできません。
815 internal async Task LoadAsync(IStreamReader fs, CancellationTokenSource tokenSource = null)
822 await this.LockAsync().ConfigureAwait(false);
824 this.FireUpdateEvent = false;
825 await this.buffer.LoadAsync(fs, tokenSource);
829 this.FireUpdateEvent = true;
835 /// ストリームに非同期モードで保存します
837 /// <param name="fs">IStreamWriterオブジェクト</param>
838 /// <param name="tokenSource">キャンセルトークン</param>
839 /// <returns>Taskオブジェクト</returns>
840 /// <remarks>非同期操作中はこのメソッドを実行することはできません</remarks>
841 internal async Task SaveAsync(IStreamWriter fs, CancellationTokenSource tokenSource = null)
845 await this.LockAsync().ConfigureAwait(false);
846 StringBuilder line = new StringBuilder();
847 for (int i = 0; i < this.Length; i++)
851 if (c == Document.NewLine || i == this.Length - 1)
853 string str = line.ToString();
854 str = str.Replace(Document.NewLine.ToString(), fs.NewLine);
855 await fs.WriteAsync(str).ConfigureAwait(false);
857 if (tokenSource != null)
858 tokenSource.Token.ThrowIfCancellationRequested();
860 System.Threading.Thread.Sleep(10);
872 /// Find()およびReplaceAll()で使用するパラメーターをセットします
874 /// <param name="pattern">検索したい文字列</param>
875 /// <param name="UseRegex">正規表現を使用するなら真</param>
876 /// <param name="opt">RegexOptions列挙体</param>
877 public void SetFindParam(string pattern, bool UseRegex, RegexOptions opt)
881 this.regex = new Regex(pattern, opt);
883 this.regex = new Regex(Regex.Escape(pattern), opt);
887 /// 現在の検索パラメーターでWatchDogを生成する
889 /// <param name="type">ハイライトタイプ</param>
890 /// <param name="color">色</param>
891 /// <returns>WatchDogオブジェクト</returns>
892 public RegexMarkerPattern CreateWatchDogByFindParam(HilightType type,Color color)
894 if (this.regex == null)
895 throw new InvalidOperationException("SetFindParam()を呼び出してください");
896 return new RegexMarkerPattern(this.regex,type,color);
902 /// <returns>見つかった場合はSearchResult列挙子を返却します</returns>
903 /// <remarks>見つかったパターン以外を置き換えた場合、正常に動作しないことがあります</remarks>
904 public IEnumerator<SearchResult> Find()
906 return this.Find(0, this.Length);
912 /// <returns>見つかった場合はSearchResult列挙子を返却します</returns>
913 /// <param name="start">開始インデックス</param>
914 /// <param name="length">検索する長さ</param>
915 /// <remarks>見つかったパターン以外を置き換えた場合、正常に動作しないことがあります</remarks>
916 public IEnumerator<SearchResult> Find(int start, int length)
918 if (this.regex == null)
919 throw new InvalidOperationException();
920 if (start < 0 || start >= this.Length)
921 throw new ArgumentOutOfRangeException();
923 int end = start + length - 1;
925 if(end > this.Length - 1)
926 throw new ArgumentOutOfRangeException();
928 StringBuilder line = new StringBuilder();
929 int oldLength = this.Length;
930 for (int i = start; i <= end; i++)
934 if (c == Document.NewLine || i == end)
936 this.match = this.regex.Match(line.ToString());
937 while (this.match.Success)
939 int startIndex = i - line.Length + 1 + this.match.Index;
940 int endIndex = startIndex + this.match.Length - 1;
942 yield return new SearchResult(this.match, startIndex, endIndex);
944 if (this.Length != oldLength) //長さが変わった場合は置き換え後のパターンの終点+1まで戻る
946 int delta = this.Length - oldLength;
947 i = endIndex + delta;
949 oldLength = this.Length;
953 this.match = this.match.NextMatch();
961 /// 任意のパターンですべて置き換えます
963 /// <param name="replacePattern">置き換え後のパターン</param>
964 /// <param name="groupReplace">グループ置き換えを行うなら真。そうでないなら偽</param>
965 public void ReplaceAll(string replacePattern,bool groupReplace)
967 if (this.regex == null)
968 throw new InvalidOperationException();
969 ReplaceAllCommand cmd = new ReplaceAllCommand(this.buffer, this.LayoutLines, this.regex, replacePattern, groupReplace);
970 this.UndoManager.push(cmd);
977 /// <param name="target">対象となる文字列</param>
978 /// <param name="pattern">置き換え後の文字列</param>
979 /// <param name="ci">大文字も文字を区別しないなら真。そうでないなら偽</param>
981 /// 検索時に大文字小文字を区別します。また、このメソッドでは正規表現を使用することはできません
983 public void ReplaceAll2(string target, string pattern,bool ci = false)
985 FastReplaceAllCommand cmd = new FastReplaceAllCommand(this.buffer, this.LayoutLines, target, pattern,ci);
986 this.UndoManager.push(cmd);
990 #region IEnumerable<char> メンバー
995 /// <returns>IEnumeratorオブジェクトを返す</returns>
996 public IEnumerator<char> GetEnumerator()
998 return this.buffer.GetEnumerator();
1003 #region IEnumerable メンバー
1005 System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
1007 throw new NotImplementedException();
1012 void buffer_Update(object sender, DocumentUpdateEventArgs e)
1016 case UpdateType.Replace:
1019 this._LayoutLines.UpdateAsReplace(e.startIndex, e.removeLength, e.insertLength);
1020 this.Markers.UpdateMarkers(e.startIndex, e.insertLength, e.removeLength);
1024 this._LayoutLines.UpdateLineAsReplace(e.row.Value, e.removeLength, e.insertLength);
1025 this.Markers.UpdateMarkers(this.LayoutLines.GetIndexFromLineNumber(e.row.Value), e.insertLength, e.removeLength);
1028 case UpdateType.Clear:
1029 this._LayoutLines.Clear();
1032 this.UpdateCalledAlways(this, e);
1033 if(this.FireUpdateEvent)
1034 this.Update(this, e);
1038 public interface IStreamReader
1046 /// ストリームから行を読み取った物を返す。LoadAsyncを呼び出す場合は必ず実装してください
1048 Task<string> ReadLineAsync();
1050 /// ストリームから指定した文字数だけ読み取る
1052 /// <param name="buffer">書き込み先バッファー</param>
1053 /// <param name="index">書き込み先バッファーのインデックス</param>
1054 /// <param name="count">読み取る文字数</param>
1055 /// <returns>読み取った文字数</returns>
1056 Task<int> ReadAsync(char[] buffer, int index, int count);
1059 public interface IStreamWriter
1062 /// ストリームに書き込む。SaveAsyncを呼び出す場合は必ず実装してください
1064 Task WriteAsync(string str);
1079 public class SearchResult
1081 private Match Match;
1098 get { return this.Match.Value; }
1102 /// 指定したパターンを置き換えて返す
1104 /// <param name="replacement">置き換える文字列</param>
1105 /// <returns>置き換え後の文字列</returns>
1106 public string Result(string replacement)
1108 return this.Match.Result(replacement);
1114 /// <param name="m">Matchオブジェクト</param>
1115 /// <param name="start">開始インデックス</param>
1116 /// <param name="end">終了インデックス</param>
1117 public SearchResult(Match m, int start,int end)
1128 public class DocumentReader : TextReader
1130 StringBuffer document;
1136 /// <param name="doc"></param>
1137 internal DocumentReader(StringBuffer doc)
1140 throw new ArgumentNullException();
1141 this.document = doc;
1147 /// <returns>文字。取得できない場合は-1</returns>
1148 public override int Peek()
1150 if (this.document == null)
1151 throw new InvalidOperationException();
1152 if (this.currentIndex >= this.document.Length)
1154 return this.document[this.currentIndex];
1158 /// 文字を取得し、イテレーターを一つ進める
1160 /// <returns>文字。取得できない場合は-1</returns>
1161 public override int Read()
1163 int c = this.Peek();
1165 this.currentIndex++;
1170 /// 文字列を読み取りバッファーに書き込む
1172 /// <param name="buffer">バッファー</param>
1173 /// <param name="index">開始インデックス</param>
1174 /// <param name="count">カウント</param>
1175 /// <returns>読み取られた文字数</returns>
1176 public override int Read(char[] buffer, int index, int count)
1178 if (this.document == null)
1179 throw new InvalidOperationException();
1182 throw new ArgumentNullException();
1184 if (this.document.Length < count)
1185 throw new ArgumentException();
1187 if (index < 0 || count < 0)
1188 throw new ArgumentOutOfRangeException();
1190 if (this.document.Length == 0)
1193 int actualCount = count;
1194 if (index + count - 1 > this.document.Length - 1)
1195 actualCount = this.document.Length - index;
1197 string str = this.document.ToString(index, actualCount);
1199 for (int i = 0; i < str.Length; i++) //ToCharArray()だと戻った時に消えてしまう
1202 this.currentIndex = index + actualCount;
1210 /// <param name="disposing">真ならアンマネージドリソースを解放する</param>
1211 protected override void Dispose(bool disposing)
1213 this.document = null;