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/>.
12 using System.Text.RegularExpressions;
13 using System.Threading;
15 using System.Collections.Generic;
16 using System.Diagnostics;
17 using Slusser.Collections.Generic;
19 namespace FooEditEngine
21 internal interface ITextLayout : IDisposable
40 /// Disposeされているなら真を返す
56 /// 桁方向の座標に対応するインデックスを得る
58 /// <param name="colpos">桁方向の座標</param>
59 /// <returns>インデックス</returns>
60 /// <remarks>行番号の幅は考慮されてないのでView以外のクラスは呼び出さないでください</remarks>
61 int GetIndexFromColPostion(double colpos);
64 /// インデックスに対応する文字の幅を得る
66 /// <param name="index">インデックス</param>
67 /// <returns>文字の幅</returns>
68 double GetWidthFromIndex(int index);
71 /// インデックスに対応する桁方向の座標を得る
73 /// <param name="index">インデックス</param>
74 /// <returns>桁方向の座標</returns>
75 /// <remarks>行頭にEOFが含まれている場合、0が返ります</remarks>
76 double GetColPostionFromIndex(int index);
81 /// <param name="index">インデックス</param>
82 /// <param name="flow">真の場合は隣接するクラスターを指すように調整し、
83 /// そうでない場合は対応するクラスターの先頭を指すように調整します</param>
84 /// <returns>調整後のインデックス</returns>
85 int AlignIndexToNearestCluster(int index, AlignDirection flow);
88 internal class SpilitStringEventArgs : EventArgs
90 public Document buffer;
94 public SpilitStringEventArgs(Document buf, int index, int length,int row)
103 internal struct SyntaxInfo
105 public TokenType type;
108 public SyntaxInfo(int index, int length, TokenType type)
112 this.length = length;
116 internal enum EncloserType
124 internal class LineToIndexTableData : IDisposable
127 /// 行の先頭。正しい行の先頭位置を取得するにはGetLineHeadIndex()を使用してください
138 public SyntaxInfo[] Syntax;
139 public EncloserType EncloserType;
140 internal ITextLayout Layout;
141 public bool Dirty = false;
144 /// コンストラクター。LineToIndexTable以外のクラスで呼び出さないでください
146 public LineToIndexTableData()
151 /// コンストラクター。LineToIndexTable以外のクラスで呼び出さないでください
153 public LineToIndexTableData(int index, int length, bool lineend,bool dirty, SyntaxInfo[] syntax)
156 this.Length = length;
157 this.LineEnd = lineend;
158 this.Syntax = syntax;
159 this.EncloserType = EncloserType.None;
163 public void Dispose()
165 if(this.Layout != null)
166 this.Layout.Dispose();
170 internal delegate IList<LineToIndexTableData> SpilitStringEventHandler(object sender, SpilitStringEventArgs e);
172 internal sealed class CreateLayoutEventArgs
193 public string Content
198 public CreateLayoutEventArgs(int index, int length,string content)
201 this.Length = length;
202 this.Content = content;
207 /// 行番号とインデックスを相互変換するためのクラス
209 public sealed class LineToIndexTable : IEnumerable<string>
211 const int MaxEntries = 100;
212 Queue<ITextLayout> CacheEntries = new Queue<ITextLayout>();
213 GapBuffer<LineToIndexTableData> Lines = new GapBuffer<LineToIndexTableData>();
215 long lastUpdateTicks = DateTime.Now.Ticks;
216 const long AllowCallTicks = 1000 * 10000; //see.DateTime.Ticks プロパティ
219 int stepRow = -1,stepLength = 0;
220 const int STEP_ROW_IS_NONE = -1;
222 internal LineToIndexTable(Document buf)
225 this.Document.Markers.Updated += Markers_Updated;
226 this.FoldingCollection = new FoldingCollection();
228 #if DEBUG && !NETFX_CORE
229 if (!Debugger.IsAttached)
231 Guid guid = Guid.NewGuid();
232 string path = string.Format("{0}\\footextbox_lti_debug_{1}.log", System.IO.Path.GetTempPath(), guid);
233 Debug.Listeners.Add(new TextWriterTraceListener(path));
234 Debug.AutoFlush = true;
239 void Markers_Updated(object sender, EventArgs e)
241 this.ClearLayoutCache();
245 /// ITextRenderインターフェイスのインスタンス。必ずセットすること
247 internal ITextRender Render
249 get { return this.render; }
256 internal SpilitStringEventHandler SpilitString;
263 get { return this.Lines.Count; }
267 /// 折り畳み関係の情報を収めたコレクション
269 public FoldingCollection FoldingCollection
278 internal IHilighter Hilighter { get; set; }
280 internal IFoldingStrategy FoldingStrategy { get; set; }
283 /// 保持しているレイアウトキャッシュをクリアーする
285 public void ClearLayoutCache()
287 foreach (ITextLayout data in this.CacheEntries)
291 this.CacheEntries.Clear();
297 /// <param name="n"></param>
298 /// <returns></returns>
299 public string this[int n]
303 LineToIndexTableData data = this.Lines[n];
304 string str = this.Document.ToString(this.GetLineHeadIndex(n), data.Length);
313 public bool IsFrozneDirtyFlag
319 int GetLineHeadIndex(int row)
321 if (this.Lines.Count == 0)
323 if (this.stepRow != STEP_ROW_IS_NONE && row > this.stepRow)
324 return this.Lines[row].Index + this.stepLength;
326 return this.Lines[row].Index;
329 internal LineToIndexTableData CreateLineToIndexTableData(int index, int length, bool lineend, SyntaxInfo[] syntax)
331 LineToIndexTableData result = new LineToIndexTableData(index, length, lineend,this.IsFrozneDirtyFlag == false, syntax);
335 internal void UpdateLineAsReplace(int row,int removedLength, int insertedLength)
337 int deltaLength = insertedLength - removedLength;
339 this.Lines[row] = new LineToIndexTableData(this.GetLineHeadIndex(row), this.GetLengthFromLineNumber(row) + deltaLength, true, true, null);
342 this.UpdateLineHeadIndex(deltaLength, row, 1);
344 this.FoldingCollection.UpdateData(this.Document, this.GetLineHeadIndex(row), insertedLength, removedLength);
346 this._IsSync = false;
348 this.lastUpdateTicks = DateTime.Now.Ticks;
351 internal void UpdateAsReplace(int index, int removedLength, int insertedLength)
354 Debug.WriteLine("Replaced Index:{0} RemoveLength:{1} InsertLength:{2}", index, removedLength, insertedLength);
356 int startRow, endRow;
357 GetRemoveRange(index, removedLength, out startRow, out endRow);
359 int deltaLength = insertedLength - removedLength;
361 var result = GetAnalyzeLength(startRow, endRow, index, removedLength, insertedLength);
362 int HeadIndex = result.Item1;
363 int analyzeLength = result.Item2;
365 //挿入範囲内のドキュメントから行を生成する
366 SpilitStringEventArgs e = new SpilitStringEventArgs(this.Document, HeadIndex, analyzeLength, startRow);
367 IList<LineToIndexTableData> newLines = SpilitString(this, e);
369 //消すべき行が複数ある場合は消すが、そうでない場合は最適化のため長さを変えるだけにとどめておく
370 int removeCount = endRow - startRow + 1;
371 if (removeCount == 1 && newLines.Count == 1)
373 this.Lines[startRow] = newLines.First();
377 this.RemoveLine(startRow, removeCount);
380 this.InsertLine(startRow, newLines, removeCount, deltaLength);
384 this.UpdateLineHeadIndex(deltaLength, startRow, newLines.Count);
388 this.FoldingCollection.UpdateData(this.Document, index, insertedLength, removedLength);
390 this._IsSync = false;
392 this.lastUpdateTicks = DateTime.Now.Ticks;
395 void GetRemoveRange(int index,int length,out int startRow,out int endRow)
397 startRow = this.GetLineNumberFromIndex(index);
398 while (startRow > 0 && this.Lines[startRow - 1].LineEnd == false)
401 endRow = this.GetLineNumberFromIndex(index + length);
402 while (endRow < this.Lines.Count && this.Lines[endRow].LineEnd == false)
404 if (endRow >= this.Lines.Count)
405 endRow = this.Lines.Count - 1;
408 Tuple<int,int> GetAnalyzeLength(int startRow,int endRow,int updateStartIndex,int removedLength,int insertedLength)
410 int HeadIndex = this.GetIndexFromLineNumber(startRow);
411 int LastIndex = this.GetIndexFromLineNumber(endRow) + this.GetLengthFromLineNumber(endRow) - 1;
413 //SpilitStringの対象となる範囲を求める
414 int fisrtPartLength = updateStartIndex - HeadIndex;
415 int secondPartLength = LastIndex - (updateStartIndex + removedLength - 1);
416 int analyzeLength = fisrtPartLength + secondPartLength + insertedLength;
417 Debug.Assert(analyzeLength <= this.Document.Length - 1 - HeadIndex + 1);
419 return new Tuple<int, int>(HeadIndex, analyzeLength);
422 void RemoveLine(int startRow, int removeCount)
424 for (int i = startRow; i < startRow + removeCount; i++)
425 this.Lines[i].Dispose();
427 this.Lines.RemoveRange(startRow, removeCount);
430 void InsertLine(int startRow, IList<LineToIndexTableData> collection,int removeCount, int deltaLength)
432 int newCount = collection.Count;
433 if (this.stepRow > startRow && newCount > 0 && newCount != removeCount)
435 //stepRowは1か2のうち、大きな方になる
436 // 1.stepRow - (削除された行数 - 挿入された行数)
438 //行が削除や置換された場合、1の処理をしないと正しいIndexが求められない
439 this.stepRow = Math.Max(this.stepRow - (removeCount - newCount), startRow);
441 if (this.stepRow < 0 || this.stepRow > this.Lines.Count + newCount)
443 Debug.WriteLine("step row < 0 or step row >= lines.count");
449 //startRowが挿入した行の開始位置なのであらかじめ引いておく
450 for (int i = 1; i < collection.Count; i++)
452 if (this.stepRow != STEP_ROW_IS_NONE && startRow + i > this.stepRow)
453 collection[i].Index -= deltaLength + this.stepLength;
455 collection[i].Index -= deltaLength;
458 this.Lines.InsertRange(startRow, collection);
463 LineToIndexTableData dummyLine = null;
464 if (this.Lines.Count == 0)
466 dummyLine = new LineToIndexTableData();
467 this.Lines.Add(dummyLine);
471 int lastLineRow = this.Lines.Count > 0 ? this.Lines.Count - 1 : 0;
472 int lastLineHeadIndex = this.GetIndexFromLineNumber(lastLineRow);
473 int lastLineLength = this.GetLengthFromLineNumber(lastLineRow);
475 if (lastLineLength != 0 && this.Document[Document.Length - 1] == Document.NewLine)
477 int realIndex = lastLineHeadIndex + lastLineLength;
478 if (lastLineRow >= this.stepRow)
479 realIndex -= this.stepLength;
480 dummyLine = new LineToIndexTableData(realIndex, 0, true,false, null);
481 this.Lines.Add(dummyLine);
485 void UpdateLineHeadIndex(int deltaLength,int startRow,int insertedLineCount)
487 if (this.Lines.Count == 0)
489 this.stepRow = STEP_ROW_IS_NONE;
494 if (this.stepRow == STEP_ROW_IS_NONE)
496 this.stepRow = startRow;
497 this.stepLength = deltaLength;
502 if (startRow < this.stepRow)
504 //ドキュメントの後半部分をごっそり削除した場合、this.stepRow >= this.Lines.Countになる可能性がある
505 if (this.stepRow >= this.Lines.Count)
506 this.stepRow = this.Lines.Count - 1;
507 for (int i = this.stepRow; i > startRow; i--)
508 this.Lines[i].Index -= this.stepLength;
510 else if (startRow > this.stepRow)
512 for (int i = this.stepRow + 1; i < startRow; i++)
513 this.Lines[i].Index += this.stepLength;
516 this.stepRow = startRow;
517 this.stepLength += deltaLength;
519 this.ValidateLines();
526 for (int i = 0; i < this.Lines.Count; i++)
528 int lineHeadIndex = this.GetLineHeadIndex(i);
529 if (lineHeadIndex != nextIndex)
531 Debug.WriteLine("Invaild Line");
532 System.Diagnostics.Debugger.Break();
534 nextIndex = lineHeadIndex + this.Lines[i].Length;
542 /// <param name="row">行番号</param>
543 /// <returns>0から始まるインデックスを返す</returns>
544 public int GetIndexFromLineNumber(int row)
546 if (row < 0 || row > this.Lines.Count)
547 throw new ArgumentOutOfRangeException();
548 return this.GetLineHeadIndex(row);
554 /// <param name="row">行番号</param>
555 /// <returns>行の文字長を返します</returns>
556 public int GetLengthFromLineNumber(int row)
558 if (row < 0 || row > this.Lines.Count)
559 throw new ArgumentOutOfRangeException();
560 return this.Lines[row].Length;
566 /// <param name="row">行番号</param>
567 /// <returns>更新されていれば真。そうでなければ偽</returns>
568 public bool GetDirtyFlag(int row)
570 if (row < 0 || row > this.Lines.Count)
571 throw new ArgumentOutOfRangeException();
572 return this.Lines[row].Dirty;
575 internal ITextLayout GetLayout(int row)
577 if (this.Lines[row].Layout != null && this.Lines[row].Layout.Invaild)
579 this.Lines[row].Layout.Dispose();
580 this.Lines[row].Layout = null;
582 if (this.Lines[row].Layout == null || this.Lines[row].Layout.Disposed)
583 this.Lines[row].Layout = this.CreateLayout(row);
584 return this.Lines[row].Layout;
587 internal event EventHandler<CreateLayoutEventArgs> CreateingLayout;
589 ITextLayout CreateLayout(int row)
592 LineToIndexTableData lineData = this.Lines[row];
593 if (lineData.Length == 0)
595 layout = this.render.CreateLaytout("", null, null);
599 int lineHeadIndex = this.GetLineHeadIndex(row);
601 string content = this.Document.ToString(lineHeadIndex, lineData.Length);
603 if (this.CreateingLayout != null)
604 this.CreateingLayout(this, new CreateLayoutEventArgs(lineHeadIndex, lineData.Length,content));
606 var markerRange = from id in this.Document.Markers.IDs
607 from s in this.Document.Markers.Get(id,lineHeadIndex,lineData.Length)
608 let n = Util.ConvertAbsIndexToRelIndex(s, lineHeadIndex, lineData.Length)
610 layout = this.render.CreateLaytout(content, lineData.Syntax, markerRange);
613 if (this.CacheEntries.Count > MaxEntries)
615 ITextLayout oldItem = this.CacheEntries.Dequeue();
618 this.CacheEntries.Enqueue(layout);
627 /// <param name="index">インデックス</param>
628 /// <returns>行番号を返します</returns>
629 public int GetLineNumberFromIndex(int index)
632 throw new ArgumentOutOfRangeException("indexに負の値を設定することはできません");
634 if (index == 0 && this.Lines.Count > 0)
637 LineToIndexTableData line;
640 if (lastLineNumber < this.Lines.Count - 1)
642 line = this.Lines[lastLineNumber];
643 lineHeadIndex = this.GetLineHeadIndex(lastLineNumber);
644 if (index >= lineHeadIndex && index < lineHeadIndex + line.Length)
645 return lastLineNumber;
648 int left = 0, right = this.Lines.Count - 1, mid;
649 while (left <= right)
651 mid = (left + right) / 2;
652 line = this.Lines[mid];
653 lineHeadIndex = this.GetLineHeadIndex(mid);
654 if (index >= lineHeadIndex && index < lineHeadIndex + line.Length)
656 lastLineNumber = mid;
659 if (index < lineHeadIndex)
669 int lastRow = this.Lines.Count - 1;
670 line = this.Lines[lastRow];
671 lineHeadIndex = this.GetLineHeadIndex(lastRow);
672 if (index >= lineHeadIndex && index <= lineHeadIndex + line.Length) //最終行長+1までキャレットが移動する可能性があるので
674 lastLineNumber = this.Lines.Count - 1;
675 return lastLineNumber;
678 throw new ArgumentOutOfRangeException("該当する行が見つかりませんでした");
682 /// インデックスからテキストポイントに変換します
684 /// <param name="index">インデックス</param>
685 /// <returns>TextPoint構造体を返します</returns>
686 public TextPoint GetTextPointFromIndex(int index)
688 TextPoint tp = new TextPoint();
689 tp.row = GetLineNumberFromIndex(index);
690 tp.col = index - this.GetLineHeadIndex(tp.row);
691 Debug.Assert(tp.row < this.Lines.Count && tp.col <= this.Lines[tp.row].Length);
696 /// テキストポイントからインデックスに変換します
698 /// <param name="tp">TextPoint構造体</param>
699 /// <returns>インデックスを返します</returns>
700 public int GetIndexFromTextPoint(TextPoint tp)
702 if (tp == TextPoint.Null)
703 throw new ArgumentNullException("TextPoint.Null以外の値でなければなりません");
704 if(tp.row < 0 || tp.row > this.Lines.Count)
705 throw new ArgumentOutOfRangeException("tp.rowが設定できる範囲を超えています");
706 if (tp.col < 0 || tp.col > this.Lines[tp.row].Length)
707 throw new ArgumentOutOfRangeException("tp.colが設定できる範囲を超えています");
708 return this.GetLineHeadIndex(tp.row) + tp.col;
714 /// <param name="force">ドキュメントが更新されていなくても再生成する</param>
715 /// <returns>生成された場合は真を返す</returns>
716 /// <remarks>デフォルトではドキュメントが更新されている時にだけ再生成されます</remarks>
717 public bool GenerateFolding(bool force = false)
719 if (this.Document.Length == 0 || this.Document.IsLocked)
721 long nowTick = DateTime.Now.Ticks;
722 bool sync = force || !this._IsSync;
723 if (sync || Math.Abs(nowTick - this.lastUpdateTicks) >= AllowCallTicks)
725 this.GenerateFolding(0, this.Document.Length - 1);
726 this.lastUpdateTicks = nowTick;
732 void GenerateFolding(int start, int end)
735 throw new ArgumentException("start <= endである必要があります");
736 if (this.FoldingStrategy != null)
738 //再生成するとすべて展開状態になってしまうので、閉じてるやつだけを保存しておく
739 FoldingItem[] closed_items = this.FoldingCollection.Where((e)=> { return !e.Expand; }).ToArray();
741 this.FoldingCollection.Clear();
743 var items = this.FoldingStrategy.AnalyzeDocument(this.Document, start, end)
746 int startRow = this.GetLineNumberFromIndex(item.Start);
747 int endRow = this.GetLineNumberFromIndex(item.End);
748 return startRow != endRow;
750 .Select((item) => item);
751 this.FoldingCollection.AddRange(items);
753 this.FoldingCollection.ApplyExpandStatus(closed_items);
758 /// フォールディングをすべて削除します
760 public void ClearFolding()
762 this.FoldingCollection.Clear();
763 this._IsSync = false;
767 /// すべての行に対しシンタックスハイライトを行います
769 public bool HilightAll(bool force = false)
771 if (this.Hilighter == null || this.Document.IsLocked)
774 long nowTick = DateTime.Now.Ticks;
775 bool sync = force || !this._IsSync;
776 if (sync || Math.Abs(nowTick - this.lastUpdateTicks) >= AllowCallTicks)
778 for (int i = 0; i < this.Lines.Count; i++)
781 this.Hilighter.Reset();
782 this.ClearLayoutCache();
784 this.lastUpdateTicks = nowTick;
792 /// ハイライト関連の情報をすべて削除します
794 public void ClearHilight()
796 foreach (LineToIndexTableData line in this.Lines)
798 this.ClearLayoutCache();
804 internal void Clear()
806 this.ClearLayoutCache();
807 this.FoldingCollection.Clear();
809 LineToIndexTableData dummy = new LineToIndexTableData();
810 this.Lines.Add(dummy);
811 this.stepRow = STEP_ROW_IS_NONE;
813 Debug.WriteLine("Clear");
816 private void HilightLine(int row)
819 List<SyntaxInfo> syntax = new List<SyntaxInfo>();
820 string str = this[row];
821 int level = this.Hilighter.DoHilight(str, str.Length, (s) =>
823 if (s.type == TokenType.None || s.type == TokenType.Control)
825 if (str[s.index + s.length - 1] == Document.NewLine)
827 syntax.Add(new SyntaxInfo(s.index, s.length, s.type));
830 LineToIndexTableData lineData = this.Lines[row];
831 lineData.Syntax = syntax.ToArray();
835 #region IEnumerable<string> メンバー
838 /// コレクションを反復処理するためのIEnumeratorを返す
840 /// <returns>IEnumeratorオブジェクト</returns>
841 public IEnumerator<string> GetEnumerator()
843 for (int i = 0; i < this.Lines.Count; i++)
844 yield return this[i];
849 #region IEnumerable メンバー
851 System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
853 for (int i = 0; i < this.Lines.Count; i++)
854 yield return this[i];