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 using System.Text.RegularExpressions;
\r
13 using System.Threading;
\r
15 using System.Collections.Generic;
\r
16 using System.Diagnostics;
\r
17 using Slusser.Collections.Generic;
\r
19 namespace FooEditEngine
\r
21 internal interface ITextLayout : IDisposable
\r
40 /// Disposeされているなら真を返す
\r
48 /// 破棄すべきなら真。そうでなければ偽
\r
56 /// 桁方向の座標に対応するインデックスを得る
\r
58 /// <param name="colpos">桁方向の座標</param>
\r
59 /// <returns>インデックス</returns>
\r
60 /// <remarks>行番号の幅は考慮されてないのでView以外のクラスは呼び出さないでください</remarks>
\r
61 int GetIndexFromColPostion(double colpos);
\r
64 /// インデックスに対応する文字の幅を得る
\r
66 /// <param name="index">インデックス</param>
\r
67 /// <returns>文字の幅</returns>
\r
68 double GetWidthFromIndex(int index);
\r
71 /// インデックスに対応する桁方向の座標を得る
\r
73 /// <param name="index">インデックス</param>
\r
74 /// <returns>桁方向の座標</returns>
\r
75 /// <remarks>行頭にEOFが含まれている場合、0が返ります</remarks>
\r
76 double GetColPostionFromIndex(int index);
\r
79 /// 適切な位置にインデックスを調整する
\r
81 /// <param name="index">インデックス</param>
\r
82 /// <param name="flow">真の場合は隣接するクラスターを指すように調整し、
\r
83 /// そうでない場合は対応するクラスターの先頭を指すように調整します</param>
\r
84 /// <returns>調整後のインデックス</returns>
\r
85 int AlignIndexToNearestCluster(int index, AlignDirection flow);
\r
88 internal class SpilitStringEventArgs : EventArgs
\r
90 public Document buffer;
\r
94 public SpilitStringEventArgs(Document buf, int index, int length,int row)
\r
98 this.length = length;
\r
103 internal struct SyntaxInfo
\r
105 public TokenType type;
\r
108 public SyntaxInfo(int index, int length, TokenType type)
\r
111 this.index = index;
\r
112 this.length = length;
\r
116 internal enum EncloserType
\r
124 internal class LineToIndexTableData : IDisposable
\r
127 /// 行の先頭。正しい行の先頭位置を取得するにはGetLineHeadIndex()を使用してください
\r
135 /// 改行マークかEOFなら真を返す
\r
137 public bool LineEnd;
\r
138 public SyntaxInfo[] Syntax;
\r
139 public EncloserType EncloserType;
\r
140 internal ITextLayout Layout;
\r
141 public bool Dirty = false;
\r
144 /// コンストラクター。LineToIndexTable以外のクラスで呼び出さないでください
\r
146 public LineToIndexTableData()
\r
151 /// コンストラクター。LineToIndexTable以外のクラスで呼び出さないでください
\r
153 public LineToIndexTableData(int index, int length, bool lineend,bool dirty, SyntaxInfo[] syntax)
\r
155 this.Index = index;
\r
156 this.Length = length;
\r
157 this.LineEnd = lineend;
\r
158 this.Syntax = syntax;
\r
159 this.EncloserType = EncloserType.None;
\r
160 this.Dirty = dirty;
\r
163 public void Dispose()
\r
165 if(this.Layout != null)
\r
166 this.Layout.Dispose();
\r
170 internal delegate IList<LineToIndexTableData> SpilitStringEventHandler(object sender, SpilitStringEventArgs e);
\r
172 internal sealed class CreateLayoutEventArgs
\r
193 public string Content
\r
198 public CreateLayoutEventArgs(int index, int length,string content)
\r
200 this.Index = index;
\r
201 this.Length = length;
\r
202 this.Content = content;
\r
207 /// 行番号とインデックスを相互変換するためのクラス
\r
209 public sealed class LineToIndexTable : IEnumerable<string>
\r
211 const int MaxEntries = 100;
\r
212 Queue<ITextLayout> CacheEntries = new Queue<ITextLayout>();
\r
213 GapBuffer<LineToIndexTableData> Lines = new GapBuffer<LineToIndexTableData>();
\r
215 long lastUpdateTicks = DateTime.Now.Ticks;
\r
216 const long AllowCallTicks = 1000 * 10000; //see.DateTime.Ticks プロパティ
\r
218 ITextRender render;
\r
219 int stepRow = -1,stepLength = 0;
\r
220 const int STEP_ROW_IS_NONE = -1;
\r
222 internal LineToIndexTable(Document buf)
\r
224 this.Document = buf;
\r
225 this.Document.Markers.Updated += Markers_Updated;
\r
226 this.FoldingCollection = new FoldingCollection();
\r
227 this._IsSync = true;
\r
228 #if DEBUG && !NETFX_CORE
\r
229 if (!Debugger.IsAttached)
\r
231 Guid guid = Guid.NewGuid();
\r
232 string path = string.Format("{0}\\footextbox_lti_debug_{1}.log", System.IO.Path.GetTempPath(), guid);
\r
233 Debug.Listeners.Add(new TextWriterTraceListener(path));
\r
234 Debug.AutoFlush = true;
\r
239 void Markers_Updated(object sender, EventArgs e)
\r
241 this.ClearLayoutCache();
\r
245 /// ITextRenderインターフェイスのインスタンス。必ずセットすること
\r
247 internal ITextRender Render
\r
249 get { return this.render; }
\r
252 this.render = value;
\r
256 internal SpilitStringEventHandler SpilitString;
\r
263 get { return this.Lines.Count; }
\r
267 /// 折り畳み関係の情報を収めたコレクション
\r
269 public FoldingCollection FoldingCollection
\r
278 internal IHilighter Hilighter { get; set; }
\r
280 internal IFoldingStrategy FoldingStrategy { get; set; }
\r
283 /// 保持しているレイアウトキャッシュをクリアーする
\r
285 public void ClearLayoutCache()
\r
287 foreach (ITextLayout data in this.CacheEntries)
\r
291 this.CacheEntries.Clear();
\r
295 /// 行番号に対応する文字列を返します
\r
297 /// <param name="n"></param>
\r
298 /// <returns></returns>
\r
299 public string this[int n]
\r
303 LineToIndexTableData data = this.Lines[n];
\r
304 string str = this.Document.ToString(this.GetLineHeadIndex(n), data.Length);
\r
313 public bool IsFrozneDirtyFlag
\r
319 int GetLineHeadIndex(int row)
\r
321 if (this.Lines.Count == 0)
\r
323 if (this.stepRow != STEP_ROW_IS_NONE && row > this.stepRow)
\r
324 return this.Lines[row].Index + this.stepLength;
\r
326 return this.Lines[row].Index;
\r
329 internal LineToIndexTableData CreateLineToIndexTableData(int index, int length, bool lineend, SyntaxInfo[] syntax)
\r
331 LineToIndexTableData result = new LineToIndexTableData(index, length, lineend,this.IsFrozneDirtyFlag == false, syntax);
\r
335 internal void UpdateAsReplace(int index, int removedLength, int insertedLength)
\r
338 Debug.WriteLine("Replaced Index:{0} RemoveLength:{1} InsertLength:{2}", index, removedLength, insertedLength);
\r
340 int startRow, endRow;
\r
341 GetRemoveRange(index, removedLength, out startRow, out endRow);
\r
343 var result = GetAnalyzeLength(startRow, endRow, index, removedLength, insertedLength);
\r
344 int HeadIndex = result.Item1;
\r
345 int analyzeLength = result.Item2;
\r
347 //挿入範囲内のドキュメントから行を生成する
\r
348 SpilitStringEventArgs e = new SpilitStringEventArgs(this.Document, HeadIndex, analyzeLength, startRow);
\r
349 IList<LineToIndexTableData> newLines = SpilitString(this, e);
\r
351 //消すべき行が複数ある場合は消すが、そうでない場合は最適化のため長さを変えるだけにとどめておく
\r
352 int removeCount = endRow - startRow + 1;
\r
353 int deltaLength = insertedLength - removedLength;
\r
354 if(removeCount == 1 && newLines.Count == 1)
\r
356 this.Lines[startRow] = newLines.First();
\r
360 this.RemoveLine(startRow, removeCount);
\r
363 this.InsertLine(startRow, newLines, removeCount, deltaLength);
\r
367 this.UpdateLineHeadIndex(deltaLength, startRow, newLines.Count);
\r
369 this.AddDummyLine();
\r
371 this.FoldingCollection.UpdateData(this.Document, index, insertedLength, removedLength);
\r
373 this._IsSync = false;
\r
375 this.lastUpdateTicks = DateTime.Now.Ticks;
\r
378 void GetRemoveRange(int index,int length,out int startRow,out int endRow)
\r
380 startRow = this.GetLineNumberFromIndex(index);
\r
381 while (startRow > 0 && this.Lines[startRow - 1].LineEnd == false)
\r
384 endRow = this.GetLineNumberFromIndex(index + length);
\r
385 while (endRow < this.Lines.Count && this.Lines[endRow].LineEnd == false)
\r
387 if (endRow >= this.Lines.Count)
\r
388 endRow = this.Lines.Count - 1;
\r
391 Tuple<int,int> GetAnalyzeLength(int startRow,int endRow,int updateStartIndex,int removedLength,int insertedLength)
\r
393 int HeadIndex = this.GetIndexFromLineNumber(startRow);
\r
394 int LastIndex = this.GetIndexFromLineNumber(endRow) + this.GetLengthFromLineNumber(endRow) - 1;
\r
396 //SpilitStringの対象となる範囲を求める
\r
397 int fisrtPartLength = updateStartIndex - HeadIndex;
\r
398 int secondPartLength = LastIndex - (updateStartIndex + removedLength - 1);
\r
399 int analyzeLength = fisrtPartLength + secondPartLength + insertedLength;
\r
400 Debug.Assert(analyzeLength <= this.Document.Length - 1 - HeadIndex + 1);
\r
402 return new Tuple<int, int>(HeadIndex, analyzeLength);
\r
405 void RemoveLine(int startRow, int removeCount)
\r
407 for (int i = startRow; i < startRow + removeCount; i++)
\r
408 this.Lines[i].Dispose();
\r
410 this.Lines.RemoveRange(startRow, removeCount);
\r
413 void InsertLine(int startRow, IList<LineToIndexTableData> collection,int removeCount, int deltaLength)
\r
415 int newCount = collection.Count;
\r
416 if (this.stepRow > startRow && newCount > 0 && newCount != removeCount)
\r
418 //stepRowは1か2のうち、大きな方になる
\r
419 // 1.stepRow - (削除された行数 - 挿入された行数)
\r
421 //行が削除や置換された場合、1の処理をしないと正しいIndexが求められない
\r
422 this.stepRow = Math.Max(this.stepRow - (removeCount - newCount), startRow);
\r
424 if (this.stepRow < 0 || this.stepRow > this.Lines.Count + newCount)
\r
426 Debug.WriteLine("step row < 0 or step row >= lines.count");
\r
432 //startRowが挿入した行の開始位置なのであらかじめ引いておく
\r
433 for (int i = 1; i < collection.Count; i++)
\r
435 if (this.stepRow != STEP_ROW_IS_NONE && startRow + i > this.stepRow)
\r
436 collection[i].Index -= deltaLength + this.stepLength;
\r
438 collection[i].Index -= deltaLength;
\r
441 this.Lines.InsertRange(startRow, collection);
\r
444 void AddDummyLine()
\r
446 LineToIndexTableData dummyLine = null;
\r
447 if (this.Lines.Count == 0)
\r
449 dummyLine = new LineToIndexTableData();
\r
450 this.Lines.Add(dummyLine);
\r
454 int lastLineRow = this.Lines.Count > 0 ? this.Lines.Count - 1 : 0;
\r
455 int lastLineHeadIndex = this.GetIndexFromLineNumber(lastLineRow);
\r
456 int lastLineLength = this.GetLengthFromLineNumber(lastLineRow);
\r
458 if (lastLineLength != 0 && this.Document[Document.Length - 1] == Document.NewLine)
\r
460 int realIndex = lastLineHeadIndex + lastLineLength;
\r
461 if (lastLineRow >= this.stepRow)
\r
462 realIndex -= this.stepLength;
\r
463 dummyLine = new LineToIndexTableData(realIndex, 0, true,false, null);
\r
464 this.Lines.Add(dummyLine);
\r
468 void UpdateLineHeadIndex(int deltaLength,int startRow,int insertedLineCount)
\r
470 if (this.Lines.Count == 0)
\r
472 this.stepRow = STEP_ROW_IS_NONE;
\r
473 this.stepLength = 0;
\r
477 if (this.stepRow == STEP_ROW_IS_NONE)
\r
479 this.stepRow = startRow;
\r
480 this.stepLength = deltaLength;
\r
485 if (startRow < this.stepRow)
\r
487 //ドキュメントの後半部分をごっそり削除した場合、this.stepRow >= this.Lines.Countになる可能性がある
\r
488 if (this.stepRow >= this.Lines.Count)
\r
489 this.stepRow = this.Lines.Count - 1;
\r
490 for (int i = this.stepRow; i > startRow; i--)
\r
491 this.Lines[i].Index -= this.stepLength;
\r
493 else if (startRow > this.stepRow)
\r
495 for (int i = this.stepRow + 1; i < startRow; i++)
\r
496 this.Lines[i].Index += this.stepLength;
\r
499 this.stepRow = startRow;
\r
500 this.stepLength += deltaLength;
\r
502 this.ValidateLines();
\r
505 void ValidateLines()
\r
509 for (int i = 0; i < this.Lines.Count; i++)
\r
511 int lineHeadIndex = this.GetLineHeadIndex(i);
\r
512 if (lineHeadIndex != nextIndex)
\r
514 Debug.WriteLine("Invaild Line");
\r
515 System.Diagnostics.Debugger.Break();
\r
517 nextIndex = lineHeadIndex + this.Lines[i].Length;
\r
523 /// 行番号をインデックスに変換します
\r
525 /// <param name="row">行番号</param>
\r
526 /// <returns>0から始まるインデックスを返す</returns>
\r
527 public int GetIndexFromLineNumber(int row)
\r
529 if (row < 0 || row > this.Lines.Count)
\r
530 throw new ArgumentOutOfRangeException();
\r
531 return this.GetLineHeadIndex(row);
\r
537 /// <param name="row">行番号</param>
\r
538 /// <returns>行の文字長を返します</returns>
\r
539 public int GetLengthFromLineNumber(int row)
\r
541 if (row < 0 || row > this.Lines.Count)
\r
542 throw new ArgumentOutOfRangeException();
\r
543 return this.Lines[row].Length;
\r
549 /// <param name="row">行番号</param>
\r
550 /// <returns>更新されていれば真。そうでなければ偽</returns>
\r
551 public bool GetDirtyFlag(int row)
\r
553 if (row < 0 || row > this.Lines.Count)
\r
554 throw new ArgumentOutOfRangeException();
\r
555 return this.Lines[row].Dirty;
\r
558 internal ITextLayout GetLayout(int row)
\r
560 if (this.Lines[row].Layout != null && this.Lines[row].Layout.Invaild)
\r
562 this.Lines[row].Layout.Dispose();
\r
563 this.Lines[row].Layout = null;
\r
565 if (this.Lines[row].Layout == null || this.Lines[row].Layout.Disposed)
\r
566 this.Lines[row].Layout = this.CreateLayout(row);
\r
567 return this.Lines[row].Layout;
\r
570 internal event EventHandler<CreateLayoutEventArgs> CreateingLayout;
\r
572 ITextLayout CreateLayout(int row)
\r
574 ITextLayout layout;
\r
575 LineToIndexTableData lineData = this.Lines[row];
\r
576 if (lineData.Length == 0)
\r
578 layout = this.render.CreateLaytout("", null, null);
\r
582 int lineHeadIndex = this.GetLineHeadIndex(row);
\r
584 string content = this.Document.ToString(lineHeadIndex, lineData.Length);
\r
586 if (this.CreateingLayout != null)
\r
587 this.CreateingLayout(this, new CreateLayoutEventArgs(lineHeadIndex, lineData.Length,content));
\r
589 var markerRange = from id in this.Document.Markers.IDs
\r
590 from s in this.Document.Markers.Get(id,lineHeadIndex,lineData.Length)
\r
591 let n = Util.ConvertAbsIndexToRelIndex(s, lineHeadIndex, lineData.Length)
\r
593 layout = this.render.CreateLaytout(content, lineData.Syntax, markerRange);
\r
596 if (this.CacheEntries.Count > MaxEntries)
\r
598 ITextLayout oldItem = this.CacheEntries.Dequeue();
\r
601 this.CacheEntries.Enqueue(layout);
\r
606 int lastLineNumber;
\r
608 /// インデックスを行番号に変換します
\r
610 /// <param name="index">インデックス</param>
\r
611 /// <returns>行番号を返します</returns>
\r
612 public int GetLineNumberFromIndex(int index)
\r
615 throw new ArgumentOutOfRangeException("indexに負の値を設定することはできません");
\r
617 if (index == 0 && this.Lines.Count > 0)
\r
620 LineToIndexTableData line;
\r
623 if (lastLineNumber < this.Lines.Count - 1)
\r
625 line = this.Lines[lastLineNumber];
\r
626 lineHeadIndex = this.GetLineHeadIndex(lastLineNumber);
\r
627 if (index >= lineHeadIndex && index < lineHeadIndex + line.Length)
\r
628 return lastLineNumber;
\r
631 int left = 0, right = this.Lines.Count - 1, mid;
\r
632 while (left <= right)
\r
634 mid = (left + right) / 2;
\r
635 line = this.Lines[mid];
\r
636 lineHeadIndex = this.GetLineHeadIndex(mid);
\r
637 if (index >= lineHeadIndex && index < lineHeadIndex + line.Length)
\r
639 lastLineNumber = mid;
\r
642 if (index < lineHeadIndex)
\r
652 int lastRow = this.Lines.Count - 1;
\r
653 line = this.Lines[lastRow];
\r
654 lineHeadIndex = this.GetLineHeadIndex(lastRow);
\r
655 if (index >= lineHeadIndex && index <= lineHeadIndex + line.Length) //最終行長+1までキャレットが移動する可能性があるので
\r
657 lastLineNumber = this.Lines.Count - 1;
\r
658 return lastLineNumber;
\r
661 throw new ArgumentOutOfRangeException("該当する行が見つかりませんでした");
\r
665 /// インデックスからテキストポイントに変換します
\r
667 /// <param name="index">インデックス</param>
\r
668 /// <returns>TextPoint構造体を返します</returns>
\r
669 public TextPoint GetTextPointFromIndex(int index)
\r
671 TextPoint tp = new TextPoint();
\r
672 tp.row = GetLineNumberFromIndex(index);
\r
673 tp.col = index - this.GetLineHeadIndex(tp.row);
\r
674 Debug.Assert(tp.row < this.Lines.Count && tp.col <= this.Lines[tp.row].Length);
\r
679 /// テキストポイントからインデックスに変換します
\r
681 /// <param name="tp">TextPoint構造体</param>
\r
682 /// <returns>インデックスを返します</returns>
\r
683 public int GetIndexFromTextPoint(TextPoint tp)
\r
685 if (tp == TextPoint.Null)
\r
686 throw new ArgumentNullException("TextPoint.Null以外の値でなければなりません");
\r
687 if(tp.row < 0 || tp.row > this.Lines.Count)
\r
688 throw new ArgumentOutOfRangeException("tp.rowが設定できる範囲を超えています");
\r
689 if (tp.col < 0 || tp.col > this.Lines[tp.row].Length)
\r
690 throw new ArgumentOutOfRangeException("tp.colが設定できる範囲を超えています");
\r
691 return this.GetLineHeadIndex(tp.row) + tp.col;
\r
695 /// フォールディングを再生成します
\r
697 /// <param name="force">ドキュメントが更新されていなくても再生成する</param>
\r
698 /// <returns>生成された場合は真を返す</returns>
\r
699 /// <remarks>デフォルトではドキュメントが更新されている時にだけ再生成されます</remarks>
\r
700 public bool GenerateFolding(bool force = false)
\r
702 if (this.Document.Length == 0 || this.Document.IsLocked)
\r
704 long nowTick = DateTime.Now.Ticks;
\r
705 if (force || Math.Abs(nowTick - this.lastUpdateTicks) >= AllowCallTicks)
\r
707 this.GenerateFolding(0, this.Document.Length - 1);
\r
708 this.lastUpdateTicks = nowTick;
\r
714 void GenerateFolding(int start, int end)
\r
717 throw new ArgumentException("start <= endである必要があります");
\r
718 if (this.FoldingStrategy != null)
\r
720 //再生成するとすべて展開状態になってしまうので、閉じてるやつだけを保存しておく
\r
721 FoldingItem[] closed_items = this.FoldingCollection.Where((e)=> { return !e.Expand; }).ToArray();
\r
723 this.FoldingCollection.Clear();
\r
725 var items = this.FoldingStrategy.AnalyzeDocument(this.Document, start, end)
\r
728 int startRow = this.GetLineNumberFromIndex(item.Start);
\r
729 int endRow = this.GetLineNumberFromIndex(item.End);
\r
730 return startRow != endRow;
\r
732 .Select((item) => item);
\r
733 this.FoldingCollection.AddRange(items);
\r
735 this.FoldingCollection.ApplyExpandStatus(closed_items);
\r
740 /// フォールディングをすべて削除します
\r
742 public void ClearFolding()
\r
744 this.FoldingCollection.Clear();
\r
745 this._IsSync = false;
\r
749 /// すべての行に対しシンタックスハイライトを行います
\r
751 public bool HilightAll(bool force = false)
\r
753 if (this.Hilighter == null || this.Document.IsLocked)
\r
756 long nowTick = DateTime.Now.Ticks;
\r
757 if (force || Math.Abs(nowTick - this.lastUpdateTicks) >= AllowCallTicks)
\r
759 for (int i = 0; i < this.Lines.Count; i++)
\r
760 this.HilightLine(i);
\r
762 this.Hilighter.Reset();
\r
763 this.ClearLayoutCache();
\r
765 this.lastUpdateTicks = nowTick;
\r
773 /// ハイライト関連の情報をすべて削除します
\r
775 public void ClearHilight()
\r
777 foreach (LineToIndexTableData line in this.Lines)
\r
778 line.Syntax = null;
\r
779 this.ClearLayoutCache();
\r
785 internal void Clear()
\r
787 this.ClearLayoutCache();
\r
788 this.FoldingCollection.Clear();
\r
789 this.Lines.Clear();
\r
790 LineToIndexTableData dummy = new LineToIndexTableData();
\r
791 this.Lines.Add(dummy);
\r
792 this.stepRow = STEP_ROW_IS_NONE;
\r
793 this.stepLength = 0;
\r
794 Debug.WriteLine("Clear");
\r
797 private void HilightLine(int row)
\r
800 List<SyntaxInfo> syntax = new List<SyntaxInfo>();
\r
801 string str = this[row];
\r
802 int level = this.Hilighter.DoHilight(str, str.Length, (s) =>
\r
804 if (s.type == TokenType.None || s.type == TokenType.Control)
\r
806 if (str[s.index + s.length - 1] == Document.NewLine)
\r
808 syntax.Add(new SyntaxInfo(s.index, s.length, s.type));
\r
811 LineToIndexTableData lineData = this.Lines[row];
\r
812 lineData.Syntax = syntax.ToArray();
\r
816 #region IEnumerable<string> メンバー
\r
819 /// コレクションを反復処理するためのIEnumeratorを返す
\r
821 /// <returns>IEnumeratorオブジェクト</returns>
\r
822 public IEnumerator<string> GetEnumerator()
\r
824 for (int i = 0; i < this.Lines.Count; i++)
\r
825 yield return this[i];
\r
830 #region IEnumerable メンバー
\r
832 System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
\r
834 for (int i = 0; i < this.Lines.Count; i++)
\r
835 yield return this[i];
\r