OSDN Git Service

ファイル操作関連をDocumentExtendクラスに分離した
[fooeditengine/FooEditEngine.git] / Common / Document.cs
1 /*\r
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
5 \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
8 \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
10  */\r
11 \r
12 using System;\r
13 using System.IO;\r
14 using System.Collections.Generic;\r
15 using System.Text;\r
16 using System.Text.RegularExpressions;\r
17 using System.Threading;\r
18 using System.Threading.Tasks;\r
19 using System.Linq;\r
20 \r
21 namespace FooEditEngine\r
22 {\r
23     /// <summary>\r
24     /// 進行状況を表す列挙体\r
25     /// </summary>\r
26     public enum ProgressState\r
27     {\r
28         /// <summary>\r
29         /// 操作が開始したことを表す\r
30         /// </summary>\r
31         Start,\r
32         /// <summary>\r
33         /// 操作が終了したことを表す\r
34         /// </summary>\r
35         Complete,\r
36     }\r
37     /// <summary>\r
38     /// 非同期操作の状態を表す\r
39     /// </summary>\r
40     public enum AsyncState\r
41     {\r
42         /// <summary>\r
43         /// 非同期操作は行われていないことを表す\r
44         /// </summary>\r
45         None,\r
46         /// <summary>\r
47         /// 読み出し中であることを表す\r
48         /// </summary>\r
49         Loading,\r
50         /// <summary>\r
51         /// 書き込み中であることを表す\r
52         /// </summary>\r
53         Saving\r
54     }\r
55     /// <summary>\r
56     /// 進行状況を表すためのイベントデータ\r
57     /// </summary>\r
58     public sealed class ProgressEventArgs : EventArgs\r
59     {\r
60         /// <summary>\r
61         /// 進行状況\r
62         /// </summary>\r
63         public ProgressState state;\r
64         /// <summary>\r
65         /// コンストラクター\r
66         /// </summary>\r
67         /// <param name="state">ProgressStateオブジェクト</param>\r
68         public ProgressEventArgs(ProgressState state)\r
69         {\r
70             this.state = state;\r
71         }\r
72     }\r
73 \r
74     /// <summary>\r
75     /// 進行状況を通知するためのデリゲート\r
76     /// </summary>\r
77     /// <param name="sender">送信元クラス</param>\r
78     /// <param name="e">イベントデータ</param>\r
79     public delegate void ProgressEventHandler(object sender, ProgressEventArgs e);\r
80 \r
81     /// <summary>\r
82     /// 更新タイプを表す列挙体\r
83     /// </summary>\r
84     public enum UpdateType\r
85     {\r
86         /// <summary>\r
87         /// ドキュメントが置き換えられたことを表す\r
88         /// </summary>\r
89         Replace,\r
90         /// <summary>\r
91         /// ドキュメント全体が削除されたことを表す\r
92         /// </summary>\r
93         Clear,\r
94     }\r
95 \r
96     /// <summary>\r
97     /// 更新タイプを通知するためのイベントデータ\r
98     /// </summary>\r
99     public sealed class DocumentUpdateEventArgs : EventArgs\r
100     {\r
101         /// <summary>\r
102         /// 更新タイプ\r
103         /// </summary>\r
104         public UpdateType type;\r
105         /// <summary>\r
106         /// 開始位置\r
107         /// </summary>\r
108         public int startIndex;\r
109         /// <summary>\r
110         /// 削除された長さ\r
111         /// </summary>\r
112         public int removeLength;\r
113         /// <summary>\r
114         /// 追加された長さ\r
115         /// </summary>\r
116         public int insertLength;\r
117         /// <summary>\r
118         /// コンストラクター\r
119         /// </summary>\r
120         /// <param name="type">更新タイプ</param>\r
121         /// <param name="startIndex">開始インデックス</param>\r
122         /// <param name="removeLength">削除された長さ</param>\r
123         /// <param name="insertLength">追加された長さ</param>\r
124         public DocumentUpdateEventArgs(UpdateType type, int startIndex, int removeLength, int insertLength)\r
125         {\r
126             this.type = type;\r
127             this.startIndex = startIndex;\r
128             this.removeLength = removeLength;\r
129             this.insertLength = insertLength;\r
130         }\r
131     }\r
132 \r
133     /// <summary>\r
134     /// ドキュメントに更新があったことを伝えるためのデリゲート\r
135     /// </summary>\r
136     /// <param name="sender">送信元クラス</param>\r
137     /// <param name="e">イベントデータ</param>\r
138     public delegate void DocumentUpdateEventHandler(object sender, DocumentUpdateEventArgs e);\r
139 \r
140     /// <summary>\r
141     /// ドキュメントの管理を行う\r
142     /// </summary>\r
143     /// <remarks>この型のすべてのメソッド・プロパティはスレッドセーフです</remarks>\r
144     public sealed class Document : IEnumerable<char>, IRandomEnumrator<char>\r
145     {\r
146         const int ProgressNotifyCount = 100;\r
147         Regex regex;\r
148         Match match;\r
149         StringBuffer buffer;\r
150         bool _EnableFireUpdateEvent = true;\r
151 \r
152         /// <summary>\r
153         /// コンストラクター\r
154         /// </summary>\r
155         public Document()\r
156             : this(null)\r
157         {\r
158         }\r
159 \r
160         internal Document(Document doc)\r
161         {\r
162             if (doc == null)\r
163                 this.buffer = new StringBuffer();\r
164             else\r
165                 this.buffer = new StringBuffer(doc.buffer);\r
166             this.buffer.Update += new DocumentUpdateEventHandler(buffer_Update);\r
167             this.UpdateCalledAlways += (s, e) => { };\r
168             this.Update += new DocumentUpdateEventHandler((s, e) => { });\r
169             this.ChangeFireUpdateEvent += new EventHandler((s, e) => { });\r
170             this.Markers = new MarkerCollection(this);\r
171             this.UndoManager = new UndoManager();\r
172         }\r
173 \r
174         /// <summary>\r
175         /// ドキュメントが更新された時に呼ばれるイベント\r
176         /// </summary>\r
177         public event DocumentUpdateEventHandler Update;\r
178 \r
179         /// <summary>\r
180         /// ドキュメントが更新された時に呼びされるイベント\r
181         /// </summary>\r
182         /// <remarks>\r
183         /// FireUpdateEventの値に関わらず常に呼びされます\r
184         /// </remarks>\r
185         internal event DocumentUpdateEventHandler UpdateCalledAlways;\r
186 \r
187         /// <summary>\r
188         /// FireUpdateEventの値が変わったときに呼び出されるイベント\r
189         /// </summary>\r
190         public event EventHandler ChangeFireUpdateEvent;\r
191 \r
192         /// <summary>\r
193         /// 改行コードの内部表現\r
194         /// </summary>\r
195         public const char NewLine = '\n';\r
196 \r
197         /// <summary>\r
198         /// EOFの内部表現\r
199         /// </summary>\r
200         public const char EndOfFile = '\u001a';\r
201 \r
202         /// <summary>\r
203         /// アンドゥ管理クラスを表す\r
204         /// </summary>\r
205         public UndoManager UndoManager\r
206         {\r
207             get;\r
208             private set;\r
209         }\r
210 \r
211         /// <summary>\r
212         /// 非同期操作の状態を表す\r
213         /// </summary>\r
214         public AsyncState State\r
215         {\r
216             get;\r
217             internal set;\r
218         }\r
219 \r
220         /// <summary>\r
221         /// 文字列の長さ\r
222         /// </summary>\r
223         public int Length\r
224         {\r
225             get\r
226             {\r
227                 return this.buffer.Length;\r
228             }\r
229         }\r
230 \r
231         /// <summary>\r
232         /// 変更のたびにUpdateイベントを発生させるかどうか\r
233         /// </summary>\r
234         public bool FireUpdateEvent\r
235         {\r
236             get\r
237             {\r
238                 return this._EnableFireUpdateEvent;\r
239             }\r
240             set\r
241             {\r
242                 this._EnableFireUpdateEvent = value;\r
243                 this.ChangeFireUpdateEvent(this, null);\r
244             }\r
245         }\r
246 \r
247         /// <summary>\r
248         /// インデクサー\r
249         /// </summary>\r
250         /// <param name="i">インデックス(自然数でなければならない)</param>\r
251         /// <returns>Char型</returns>\r
252         public char this[int i]\r
253         {\r
254             get\r
255             {\r
256                 return this.buffer[i];\r
257             }\r
258         }\r
259 \r
260         /// <summary>\r
261         /// マーカーコレクション\r
262         /// </summary>\r
263         public MarkerCollection Markers\r
264         {\r
265             get;\r
266             private set;\r
267         }\r
268 \r
269         internal StringBuffer StringBuffer\r
270         {\r
271             get\r
272             {\r
273                 return this.buffer;\r
274             }\r
275         }\r
276 \r
277         /// <summary>\r
278         /// DocumentReaderを作成します\r
279         /// </summary>\r
280         /// <returns>DocumentReaderオブジェクト</returns>\r
281         public DocumentReader CreateReader()\r
282         {\r
283             return new DocumentReader(this.buffer);\r
284         }\r
285 \r
286         /// <summary>\r
287         /// マーカーを設定する\r
288         /// </summary>\r
289         /// <param name="m">設定したいマーカー</param>\r
290         public void SetMarker(Marker m)\r
291         {\r
292             if (m.start < 0 || m.start + m.length > this.Length)\r
293                 throw new ArgumentOutOfRangeException("startもしくはendが指定できる範囲を超えています");\r
294 \r
295             this.Markers.Add(m);\r
296         }\r
297 \r
298         /// <summary>\r
299         /// マーカーを削除する\r
300         /// </summary>\r
301         /// <param name="start">開始インデックス</param>\r
302         /// <param name="length">削除する長さ</param>\r
303         public void RemoveMarker(int start, int length)\r
304         {\r
305             if (start < 0 || start + length > this.Length)\r
306                 throw new ArgumentOutOfRangeException("startもしくはendが指定できる範囲を超えています");\r
307 \r
308             this.Markers.RemoveAll(start, length);\r
309         }\r
310 \r
311         /// <summary>\r
312         /// マーカーを削除する\r
313         /// </summary>\r
314         /// <param name="type">削除したいマーカーのタイプ</param>\r
315         public void RemoveMarker(HilightType type)\r
316         {\r
317             this.Markers.RemoveAll(type);\r
318         }\r
319 \r
320         /// <summary>\r
321         /// インデックスに対応するマーカーを得る\r
322         /// </summary>\r
323         /// <param name="index">インデックス</param>\r
324         /// <returns>Marker構造体の列挙子</returns>\r
325         public IEnumerable<Marker> GetMarkers(int index)\r
326         {\r
327             if (index < 0 || index > this.Length)\r
328                 throw new ArgumentOutOfRangeException("indexが範囲を超えています");\r
329             return this.Markers.Get(index);\r
330         }\r
331 \r
332         /// <summary>\r
333         /// 部分文字列を取得する\r
334         /// </summary>\r
335         /// <param name="index">開始インデックス</param>\r
336         /// <param name="length">長さ</param>\r
337         /// <returns>Stringオブジェクト</returns>\r
338         public string ToString(int index, int length)\r
339         {\r
340             return this.buffer.ToString(index, length);\r
341         }\r
342 \r
343         /// <summary>\r
344         /// インデックスを開始位置とする文字列を返す\r
345         /// </summary>\r
346         /// <param name="index">開始インデックス</param>\r
347         /// <returns>Stringオブジェクト</returns>\r
348         public string ToString(int index)\r
349         {\r
350             return this.ToString(index, this.buffer.Length - index);\r
351         }\r
352 \r
353         /// <summary>\r
354         /// 行を取得する\r
355         /// </summary>\r
356         /// <param name="startIndex">開始インデックス</param>\r
357         /// <param name="endIndex">終了インデックス</param>\r
358         /// <param name="maxCharCount">最大長</param>\r
359         /// <returns>行イテレーターが返される</returns>\r
360         public IEnumerable<string> GetLines(int startIndex, int endIndex, int maxCharCount = -1)\r
361         {\r
362             return this.buffer.GetLines(startIndex, endIndex, maxCharCount);\r
363         }\r
364 \r
365         /// <summary>\r
366         /// 文字列を追加する\r
367         /// </summary>\r
368         /// <param name="s">追加したい文字列</param>\r
369         /// <remarks>非同期操作中はこのメソッドを実行することはできません</remarks>\r
370         public void Append(string s)\r
371         {\r
372             this.Replace(this.buffer.Length, 0, s);\r
373         }\r
374 \r
375         /// <summary>\r
376         /// 文字列を挿入する\r
377         /// </summary>\r
378         /// <param name="index">開始インデックス</param>\r
379         /// <param name="s">追加したい文字列</param>\r
380         /// <remarks>読み出し操作中はこのメソッドを実行することはできません</remarks>\r
381         public void Insert(int index, string s)\r
382         {\r
383             this.Replace(index, 0, s);\r
384         }\r
385 \r
386         /// <summary>\r
387         /// 文字列を削除する\r
388         /// </summary>\r
389         /// <param name="index">開始インデックス</param>\r
390         /// <param name="length">長さ</param>\r
391         /// <remarks>読み出し操作中はこのメソッドを実行することはできません</remarks>\r
392         public void Remove(int index, int length)\r
393         {\r
394             this.Replace(index, length, "");\r
395         }\r
396 \r
397         /// <summary>\r
398         /// ドキュメントを置き換える\r
399         /// </summary>\r
400         /// <param name="index">開始インデックス</param>\r
401         /// <param name="length">長さ</param>\r
402         /// <param name="s">文字列</param>\r
403         /// <remarks>読み出し操作中はこのメソッドを実行することはできません</remarks>\r
404         public void Replace(int index, int length, string s)\r
405         {\r
406             if (this.State == AsyncState.Loading)\r
407                 throw new InvalidOperationException();\r
408             if (index < 0 || index > this.buffer.Length || index + length > this.buffer.Length || length < 0)\r
409                 throw new ArgumentOutOfRangeException();\r
410             if (length == 0 && (s == string.Empty || s == null))\r
411                 return;\r
412 \r
413             this.RemoveMarker(index, length);\r
414 \r
415             ReplaceCommand cmd = new ReplaceCommand(this.buffer, index, length, s);\r
416             this.UndoManager.push(cmd);\r
417             cmd.redo();\r
418         }\r
419 \r
420         /// <summary>\r
421         /// 物理行をすべて削除する\r
422         /// </summary>\r
423         /// <remarks>Dirtyフラグも同時にクリアーされます</remarks>\r
424         /// <remarks>非同期操作中はこのメソッドを実行することはできません</remarks>\r
425         public void Clear()\r
426         {\r
427             if (this.State == AsyncState.Loading)\r
428                 throw new InvalidOperationException();\r
429             this.buffer.Clear();\r
430         }\r
431 \r
432         /// <summary>\r
433         /// Find()で使用するパラメーターをセットします\r
434         /// </summary>\r
435         /// <param name="pattern">検索したい文字列</param>\r
436         /// <param name="UseRegex">正規表現を使用するなら真</param>\r
437         /// <param name="opt">RegexOptions列挙体</param>\r
438         public void SetFindParam(string pattern, bool UseRegex, RegexOptions opt)\r
439         {\r
440             this.match = null;\r
441             if (UseRegex)\r
442                 this.regex = new Regex(pattern, opt);\r
443             else\r
444                 this.regex = new Regex(Regex.Escape(pattern), opt);\r
445         }\r
446 \r
447         /// <summary>\r
448         /// 指定した文字列を検索します\r
449         /// </summary>\r
450         /// <returns>見つかった場合はSearchResult列挙子を返却します</returns>\r
451         /// <remarks>見つかったパターン以外を置き換えた場合、正常に動作しないことがあります</remarks>\r
452         public IEnumerator<SearchResult> Find()\r
453         {\r
454             return this.Find(0, this.Length);\r
455         }\r
456 \r
457         /// <summary>\r
458         /// 指定した文字列を検索します\r
459         /// </summary>\r
460         /// <returns>見つかった場合はSearchResult列挙子を返却します</returns>\r
461         /// <param name="start">開始インデックス</param>\r
462         /// <param name="length">検索する長さ</param>\r
463         /// <remarks>見つかったパターン以外を置き換えた場合、正常に動作しないことがあります</remarks>\r
464         public IEnumerator<SearchResult> Find(int start, int length)\r
465         {\r
466             if (this.State == AsyncState.Loading)\r
467                 throw new InvalidOperationException();\r
468             if (this.regex == null)\r
469                 throw new InvalidOperationException();\r
470             if (start < 0 || start >= this.Length)\r
471                 throw new ArgumentOutOfRangeException();\r
472 \r
473             int end = start + length - 1;\r
474 \r
475             if(end > this.Length - 1)\r
476                 throw new ArgumentOutOfRangeException();\r
477 \r
478             StringBuilder line = new StringBuilder();\r
479             int oldLength = this.Length;\r
480             for (int i = start; i <= end; i++)\r
481             {\r
482                 char c = this[i];\r
483                 line.Append(c);\r
484                 if (c == Document.NewLine || i == end)\r
485                 {\r
486                     this.match = this.regex.Match(line.ToString());\r
487                     while (this.match.Success)\r
488                     {\r
489                         int startIndex = i - line.Length + 1 + this.match.Index;\r
490                         int endIndex = startIndex + this.match.Length - 1;\r
491 \r
492                         yield return new SearchResult(this.match, startIndex, endIndex);\r
493 \r
494                         if (this.Length != oldLength)   //長さが変わった場合は置き換え後のパターンの終点+1まで戻る\r
495                         {\r
496                             int delta = this.Length - oldLength;\r
497                             i = endIndex + delta;\r
498                             end = end + delta;\r
499                             oldLength = this.Length;\r
500                             break;\r
501                         }\r
502 \r
503                         this.match = this.match.NextMatch();\r
504                     }\r
505                     line.Clear();\r
506                 }\r
507             }\r
508         }\r
509 \r
510         /// <summary>\r
511         /// 任意のパターンですべて置き換えます\r
512         /// </summary>\r
513         /// <param name="replacePattern">置き換え後のパターン</param>\r
514         /// <param name="groupReplace">グループ置き換えを行うなら真。そうでないなら偽</param>\r
515         public void ReplaceAll(string replacePattern,bool groupReplace)\r
516         {\r
517             if (this.regex == null)\r
518                 throw new InvalidOperationException();\r
519             ReplaceAllCommand cmd = new ReplaceAllCommand(this.buffer, this.regex, replacePattern, groupReplace);\r
520             this.UndoManager.push(cmd);\r
521             cmd.redo();\r
522         }\r
523 \r
524         #region IEnumerable<char> メンバー\r
525 \r
526         /// <summary>\r
527         /// 列挙子を返します\r
528         /// </summary>\r
529         /// <returns>IEnumeratorオブジェクトを返す</returns>\r
530         public IEnumerator<char> GetEnumerator()\r
531         {\r
532             return this.buffer.GetEnumerator();\r
533         }\r
534 \r
535         #endregion\r
536 \r
537         #region IEnumerable メンバー\r
538 \r
539         System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()\r
540         {\r
541             throw new NotImplementedException();\r
542         }\r
543 \r
544         #endregion\r
545 \r
546         void buffer_Update(object sender, DocumentUpdateEventArgs e)\r
547         {\r
548             this.UpdateCalledAlways(this, e);\r
549             if(this.FireUpdateEvent)\r
550                 this.Update(this, e);\r
551         }\r
552     }\r
553 \r
554     /// <summary>\r
555     /// 検索結果を表す\r
556     /// </summary>\r
557     public class SearchResult\r
558     {\r
559         private Match Match;\r
560 \r
561         /// <summary>\r
562         /// 一致した場所の開始位置を表す\r
563         /// </summary>\r
564         public int Start;\r
565 \r
566         /// <summary>\r
567         /// 一致した場所の終了位置を表す\r
568         /// </summary>\r
569         public int End;\r
570 \r
571         /// <summary>\r
572         /// 見つかった文字列を返す\r
573         /// </summary>\r
574         public string Value\r
575         {\r
576             get { return this.Match.Value; }\r
577         }\r
578 \r
579         /// <summary>\r
580         /// 指定したパターンを置き換えて返す\r
581         /// </summary>\r
582         /// <param name="replacement">置き換える文字列</param>\r
583         /// <returns>置き換え後の文字列</returns>\r
584         public string Result(string replacement)\r
585         {\r
586             return this.Match.Result(replacement);\r
587         }\r
588 \r
589         /// <summary>\r
590         /// コンストラクター\r
591         /// </summary>\r
592         /// <param name="m">Matchオブジェクト</param>\r
593         /// <param name="start">開始インデックス</param>\r
594         /// <param name="end">終了インデックス</param>\r
595         public SearchResult(Match m, int start,int end)\r
596         {\r
597             this.Match = m;\r
598             this.Start = start;\r
599             this.End = end;\r
600         }\r
601     }\r
602 \r
603     /// <summary>\r
604     /// ドキュメントリーダー\r
605     /// </summary>\r
606     public class DocumentReader : TextReader\r
607     {\r
608         StringBuffer document;      \r
609         int currentIndex;\r
610 \r
611         /// <summary>\r
612         /// コンストラクター\r
613         /// </summary>\r
614         /// <param name="doc"></param>\r
615         internal DocumentReader(StringBuffer doc)\r
616         {\r
617             if (doc == null)\r
618                 throw new ArgumentNullException();\r
619             this.document = doc;\r
620         }\r
621 \r
622         /// <summary>\r
623         /// 文字を取得する\r
624         /// </summary>\r
625         /// <returns>文字。取得できない場合は-1</returns>\r
626         public override int Peek()\r
627         {\r
628             if (this.document == null)\r
629                 throw new InvalidOperationException();\r
630             if (this.currentIndex >= this.document.Length)\r
631                 return -1;\r
632             return this.document[this.currentIndex];\r
633         }\r
634 \r
635         /// <summary>\r
636         /// 文字を取得し、イテレーターを一つ進める\r
637         /// </summary>\r
638         /// <returns>文字。取得できない場合は-1</returns>\r
639         public override int Read()\r
640         {\r
641             int c = this.Peek();\r
642             if(c != -1)\r
643                 this.currentIndex++;\r
644             return c;\r
645         }\r
646 \r
647         /// <summary>\r
648         /// 文字列を読み取りバッファーに書き込む\r
649         /// </summary>\r
650         /// <param name="buffer">バッファー</param>\r
651         /// <param name="index">開始インデックス</param>\r
652         /// <param name="count">カウント</param>\r
653         /// <returns>読み取られた文字数</returns>\r
654         public override int Read(char[] buffer, int index, int count)\r
655         {\r
656             if (this.document == null)\r
657                 throw new InvalidOperationException();\r
658 \r
659             if (buffer == null)\r
660                 throw new ArgumentNullException();\r
661 \r
662             if (this.document.Length < count)\r
663                 throw new ArgumentException();\r
664 \r
665             if (index < 0 || count < 0)\r
666                 throw new ArgumentOutOfRangeException();\r
667 \r
668             if (this.document.Length == 0)\r
669                 return 0;\r
670 \r
671             int actualCount = count;\r
672             if (index + count - 1 > this.document.Length - 1)\r
673                 actualCount = this.document.Length - index;\r
674 \r
675             string str = this.document.ToString(index, actualCount);\r
676 \r
677             for (int i = 0; i < str.Length; i++)    //ToCharArray()だと戻った時に消えてしまう\r
678                 buffer[i] = str[i];\r
679 \r
680             this.currentIndex = index + actualCount;\r
681             \r
682             return actualCount;\r
683         }\r
684 \r
685         /// <summary>\r
686         /// オブジェクトを破棄する\r
687         /// </summary>\r
688         /// <param name="disposing">真ならアンマネージドリソースを解放する</param>\r
689         protected override void Dispose(bool disposing)\r
690         {\r
691             this.document = null;\r
692         }\r
693     }\r
694 }\r