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 /// X座標に対応するインデックスを得る
\r
58 /// <param name="x">X座標</param>
\r
59 /// <returns>インデックス</returns>
\r
60 /// <remarks>行番号の幅は考慮されてないのでView以外のクラスは呼び出さないでください</remarks>
\r
61 int GetIndexFromX(double x);
\r
64 /// インデックスに対応する文字の幅を得る
\r
66 /// <param name="index">インデックス</param>
\r
67 /// <returns>文字の幅</returns>
\r
68 double GetWidthFromIndex(int index);
\r
71 /// インデックスに対応するX座標を得る
\r
73 /// <param name="index">インデックス</param>
\r
74 /// <returns>X座標</returns>
\r
75 /// <remarks>行頭にEOFが含まれている場合、0が返ります</remarks>
\r
76 double GetXFromIndex(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
126 ITextLayout _layout;
\r
129 public bool LineEnd;
\r
130 public SyntaxInfo[] Syntax;
\r
131 public EncloserType EncloserType;
\r
132 internal ITextLayout Layout
\r
136 if (this._layout != null && this._layout.Invaild)
\r
138 this._layout.Dispose();
\r
139 this._layout = null;
\r
141 if (this._layout == null || this._layout.Disposed)
\r
142 this._layout = this.CreateLayout(this.Index, this.Length, this.Syntax);
\r
146 internal Func<int, int,SyntaxInfo[], ITextLayout> CreateLayout;
\r
147 public LineToIndexTableData()
\r
150 public LineToIndexTableData(int index, int length, bool lineend, SyntaxInfo[] syntax)
\r
152 this.Index = index;
\r
153 this.Length = length;
\r
154 this.LineEnd = lineend;
\r
155 this.Syntax = syntax;
\r
156 this.EncloserType = EncloserType.None;
\r
159 public void Dispose()
\r
161 if(this._layout != null)
\r
162 this._layout.Dispose();
\r
166 internal delegate IList<LineToIndexTableData> SpilitStringEventHandler(object sender, SpilitStringEventArgs e);
\r
169 /// 行番号とインデックスを相互変換するためのクラス
\r
171 public sealed class LineToIndexTable : IEnumerable<string>
\r
173 const int MaxEntries = 100;
\r
174 Queue<ITextLayout> CacheEntries = new Queue<ITextLayout>();
\r
175 GapBuffer<LineToIndexTableData> Lines = new GapBuffer<LineToIndexTableData>();
\r
177 bool _UrlMarker,_IsSync;
\r
178 Regex urlPattern = new Regex("(http|https|ftp)(:\\/\\/[-_.!~*\\'()a-zA-Z0-9;\\/?:\\@&=+\\$,%#]+)");
\r
179 ITextRender render;
\r
181 internal LineToIndexTable(Document buf, ITextRender r)
\r
183 this.Document = buf;
\r
184 this.Document.Markers.Updated += Markers_Updated;
\r
186 this.FoldingCollection = new FoldingCollection();
\r
187 this._IsSync = true;
\r
190 void Markers_Updated(object sender, EventArgs e)
\r
192 this.ClearLayoutCache();
\r
195 internal SpilitStringEventHandler SpilitString;
\r
202 get { return this.Lines.Count; }
\r
206 /// 折り畳み関係の情報を収めたコレクション
\r
208 public FoldingCollection FoldingCollection
\r
217 internal IHilighter Hilighter { get; set; }
\r
219 internal IFoldingStrategy FoldingStrategy { get; set; }
\r
224 /// <remarks>変更を反映させるためには再描写する必要があります</remarks>
\r
225 internal bool UrlMark
\r
229 return this._UrlMarker;
\r
233 this._UrlMarker = value;
\r
234 this.Document.RemoveMarker(HilightType.Url);
\r
235 this.SetUrlMarker(0, this.Count);
\r
240 /// 保持しているレイアウトキャッシュをクリアーする
\r
242 public void ClearLayoutCache()
\r
244 foreach (ITextLayout data in this.CacheEntries)
\r
248 this.CacheEntries.Clear();
\r
252 /// 行番号に対応する文字列を返します
\r
254 /// <param name="n"></param>
\r
255 /// <returns></returns>
\r
256 public string this[int n]
\r
260 LineToIndexTableData data = this.Lines[n];
\r
261 string str = this.Document.ToString(data.Index, data.Length);
\r
267 internal void UpdateAsReplace(int index, int removedLength, int insertedLength)
\r
269 //削除すべき行の開始位置と終了位置を求める
\r
270 int startRow = this.GetLineNumberFromIndex(index);
\r
271 if (startRow > 0 && this.Lines[startRow - 1].LineEnd == false)
\r
274 int endRow = this.GetLineNumberFromIndex(index + removedLength);
\r
275 while (endRow < this.Lines.Count && this.Lines[endRow].LineEnd == false)
\r
277 if (endRow >= this.Lines.Count)
\r
278 endRow = this.Lines.Count - 1;
\r
280 //SpilitStringの対象となる範囲を求める
\r
281 int HeadIndex = this.GetIndexFromLineNumber(startRow);
\r
283 int LastIndex = this.GetIndexFromLineNumber(endRow) + this.GetLengthFromLineNumber(endRow) - 1;
\r
285 int fisrtPartLength = index - HeadIndex;
\r
287 int secondPartLength = LastIndex - (index + removedLength - 1);
\r
289 int analyzeLength = fisrtPartLength + secondPartLength + insertedLength;
\r
291 System.Diagnostics.Debug.Assert(analyzeLength <= this.Document.Length - 1 - HeadIndex + 1);
\r
294 int removeCount = endRow - startRow + 1;
\r
295 for (int i = startRow; i < removeCount; i++)
\r
296 this.Lines[i].Dispose();
\r
298 this.Lines.RemoveRange(startRow, removeCount);
\r
300 //行を分割し、削除した位置に挿入する
\r
301 SpilitStringEventArgs e = new SpilitStringEventArgs(this.Document, HeadIndex, analyzeLength, startRow);
\r
302 IList<LineToIndexTableData> newLines = SpilitString(this, e);
\r
303 foreach (LineToIndexTableData data in newLines)
\r
305 data.CreateLayout = this.LineToIndexTableData_CreatLayout;
\r
308 this.Lines.InsertRange(startRow, newLines);
\r
311 int deltaLength = insertedLength - removedLength;
\r
313 for (int i = startRow + newLines.Count; i < this.Lines.Count; i++)
\r
315 this.Lines[i].Index += deltaLength;
\r
318 this.FoldingCollection.UpdateData(this.Document, index, insertedLength, removedLength);
\r
320 LineToIndexTableData lastLine = this.Lines.Count > 0 ? this.Lines.Last() : null;
\r
321 int lastLineIndex = 0;
\r
322 if(lastLine != null)
\r
323 lastLineIndex = lastLine.Index + lastLine.Length - 1;
\r
326 LineToIndexTableData dummyLine = null;
\r
327 if (this.Lines.Count == 0)
\r
329 dummyLine = new LineToIndexTableData();
\r
330 dummyLine.CreateLayout = this.LineToIndexTableData_CreatLayout;
\r
331 this.Lines.Add(dummyLine);
\r
335 if (lastLine.Length != 0 && this.Document[lastLineIndex] == Document.NewLine)
\r
337 dummyLine = new LineToIndexTableData(lastLine.Index + lastLine.Length, 0, true, null);
\r
338 dummyLine.CreateLayout = this.LineToIndexTableData_CreatLayout;
\r
339 this.Lines.Add(dummyLine);
\r
343 this.Hilight(startRow, newLines.Count);
\r
345 SetUrlMarker(startRow, newLines.Count);
\r
347 if (lastLine != null)
\r
349 foreach (FoldingItem foldingData in this.FoldingCollection.GetRange(index, lastLineIndex))
\r
350 if (foldingData.Start == foldingData.End)
\r
351 this.FoldingCollection.Remove(foldingData);
\r
354 this._IsSync = false;
\r
357 ITextLayout LineToIndexTableData_CreatLayout(int index, int length,SyntaxInfo[] syntax)
\r
359 ITextLayout layout;
\r
362 layout = this.render.CreateLaytout("", null, null);
\r
366 var markerRange = from s in this.Document.Markers.Get(index, length)
\r
367 let n = Util.ConvertAbsIndexToRelIndex(s, index, length)
\r
369 layout = this.render.CreateLaytout(this.Document.ToString(index, length).TrimEnd(Document.NewLine), syntax, markerRange);
\r
372 if (this.CacheEntries.Count > MaxEntries)
\r
374 ITextLayout oldItem = this.CacheEntries.Dequeue();
\r
377 this.CacheEntries.Enqueue(layout);
\r
383 /// 行番号をインデックスに変換します
\r
385 /// <param name="row">行番号</param>
\r
386 /// <returns>0から始まるインデックスを返す</returns>
\r
387 public int GetIndexFromLineNumber(int row)
\r
389 if (row < 0 || row > this.Lines.Count)
\r
390 throw new ArgumentOutOfRangeException();
\r
391 return this.Lines[row].Index;
\r
397 /// <param name="row">行番号</param>
\r
398 /// <returns>行の文字長を返します</returns>
\r
399 public int GetLengthFromLineNumber(int row)
\r
401 if (row < 0 || row > this.Lines.Count)
\r
402 throw new ArgumentOutOfRangeException();
\r
403 return this.Lines[row].Length;
\r
406 internal ITextLayout GetLayout(int row)
\r
408 return this.Lines[row].Layout;
\r
411 internal LineToIndexTableData GetData(int row)
\r
413 if (row < 0 || row > this.Lines.Count)
\r
414 throw new ArgumentOutOfRangeException();
\r
415 return this.Lines[row];
\r
418 int lastLineNumber;
\r
420 /// インデックスを行番号に変換します
\r
422 /// <param name="index">インデックス</param>
\r
423 /// <returns>行番号を返します</returns>
\r
424 public int GetLineNumberFromIndex(int index)
\r
427 throw new ArgumentOutOfRangeException("indexに負の値を設定することはできません");
\r
429 if (index == 0 && this.Lines.Count > 0)
\r
432 LineToIndexTableData line;
\r
434 if (lastLineNumber < this.Lines.Count - 1)
\r
436 line = this.Lines[lastLineNumber];
\r
437 if (index >= line.Index && index < line.Index + line.Length)
\r
438 return lastLineNumber;
\r
441 int left = 0, right = this.Lines.Count - 1, mid;
\r
442 while (left <= right)
\r
444 mid = (left + right) / 2;
\r
445 line = this.Lines[mid];
\r
446 if (index >= line.Index && index < line.Index + line.Length)
\r
448 lastLineNumber = mid;
\r
451 if (index < line.Index)
\r
461 line = this.Lines.Last();
\r
462 if (index >= line.Index && index <= line.Index + line.Length) //最終行長+1までキャレットが移動する可能性があるので
\r
464 lastLineNumber = this.Lines.Count - 1;
\r
465 return lastLineNumber;
\r
468 throw new ArgumentOutOfRangeException("該当する行が見つかりませんでした");
\r
472 /// インデックスからテキストポイントに変換します
\r
474 /// <param name="index">インデックス</param>
\r
475 /// <returns>TextPoint構造体を返します</returns>
\r
476 public TextPoint GetTextPointFromIndex(int index)
\r
478 TextPoint tp = new TextPoint();
\r
479 tp.row = GetLineNumberFromIndex(index);
\r
480 tp.col = index - this.Lines[tp.row].Index;
\r
481 Debug.Assert(tp.row < this.Lines.Count && tp.col <= this.Lines[tp.row].Length);
\r
486 /// テキストポイントからインデックスに変換します
\r
488 /// <param name="tp">TextPoint構造体</param>
\r
489 /// <returns>インデックスを返します</returns>
\r
490 public int GetIndexFromTextPoint(TextPoint tp)
\r
492 if (tp == TextPoint.Null)
\r
493 throw new ArgumentNullException("TextPoint.Null以外の値でなければなりません");
\r
494 if(tp.row < 0 || tp.row > this.Lines.Count)
\r
495 throw new ArgumentOutOfRangeException("tp.rowが設定できる範囲を超えています");
\r
496 if (tp.col < 0 || tp.col > this.Lines[tp.row].Length)
\r
497 throw new ArgumentOutOfRangeException("tp.colが設定できる範囲を超えています");
\r
498 return this.Lines[tp.row].Index + tp.col;
\r
502 /// フォールディングを再生成します
\r
504 /// <param name="force">ドキュメントが更新されていなくても再生成する</param>
\r
505 /// <returns>生成された場合は真を返す</returns>
\r
506 /// <remarks>デフォルトではドキュメントが更新されている時にだけ再生成されます</remarks>
\r
507 public bool GenerateFolding(bool force = false)
\r
510 this._IsSync = false;
\r
511 if (this.Document.Length == 0 || this._IsSync)
\r
513 this.GenerateFolding(0, this.Document.Length - 1);
\r
514 this._IsSync = true;
\r
518 void GenerateFolding(int start, int end)
\r
521 throw new ArgumentException("start <= endである必要があります");
\r
522 if (this.FoldingStrategy != null)
\r
524 var items = this.FoldingStrategy.AnalyzeDocument(this.Document, start, end)
\r
527 int startRow = this.GetLineNumberFromIndex(item.Start);
\r
528 int endRow = this.GetLineNumberFromIndex(item.End);
\r
529 return startRow != endRow;
\r
531 .Select((item) => item);
\r
532 this.FoldingCollection.AddRange(items);
\r
537 /// フォールディングをすべて削除します
\r
539 public void ClearFolding()
\r
541 this.FoldingCollection.Clear();
\r
542 this._IsSync = false;
\r
546 /// すべての行に対しシンタックスハイライトを行います
\r
548 public void HilightAll()
\r
550 this.Hilight(0, this.Lines.Count);
\r
551 SetUrlMarker(0, this.Lines.Count);
\r
552 this.ClearLayoutCache();
\r
556 /// ハイライト関連の情報をすべて削除します
\r
558 public void ClearHilight()
\r
560 foreach (LineToIndexTableData line in this.Lines)
\r
561 line.Syntax = null;
\r
562 this.ClearLayoutCache();
\r
568 internal void Clear()
\r
570 this.ClearLayoutCache();
\r
571 this.FoldingCollection.Clear();
\r
572 this.Lines.Clear();
\r
573 LineToIndexTableData dummy = new LineToIndexTableData();
\r
574 dummy.CreateLayout = this.LineToIndexTableData_CreatLayout;
\r
575 this.Lines.Add(dummy);
\r
578 void Hilight(int row, int rowCount)
\r
580 if (this.Hilighter == null || rowCount == 0)
\r
583 //シンタックスハイライトの開始行を求める
\r
584 int startRow = row;
\r
585 EncloserType type = this.Lines[startRow].EncloserType;
\r
586 EncloserType prevLineType = startRow > 0 ? this.Lines[startRow - 1].EncloserType : EncloserType.None;
\r
587 if (type == EncloserType.Now || type == EncloserType.End ||
\r
588 prevLineType == EncloserType.Now || prevLineType == EncloserType.End)
\r
589 startRow = SearchStartRow(startRow);
\r
590 else if (prevLineType == EncloserType.Begin)
\r
591 startRow = startRow - 1;
\r
593 //シンタックスハイライトの終了行を求める
\r
594 int endRow = row + rowCount - 1;
\r
595 type = this.Lines[endRow].EncloserType;
\r
596 prevLineType = endRow > 0 ? this.Lines[endRow - 1].EncloserType : EncloserType.None;
\r
597 if (type == EncloserType.Begin || type == EncloserType.Now ||
\r
598 prevLineType == EncloserType.Begin || prevLineType == EncloserType.Now)
\r
599 endRow = SearchEndRow(endRow);
\r
600 else if (endRow + 1 <= this.Lines.Count - 1 && this.Lines[endRow + 1].EncloserType == EncloserType.Now)
\r
601 endRow = SearchEndRow(endRow + 1);
\r
604 bool hasBeginEncloser = false;
\r
606 for (i = startRow; i <= endRow; i++)
\r
608 this.HilightLine(i, ref hasBeginEncloser);
\r
611 if (hasBeginEncloser) //終了エンクロージャーが見つかったかどうか
\r
613 for (; i < this.Lines.Count; i++)
\r
615 if (this.HilightLine(i, ref hasBeginEncloser) < 0)
\r
620 this.Hilighter.Reset();
\r
623 private int HilightLine(int row, ref bool hasBeginEncloser)
\r
626 List<SyntaxInfo> syntax = new List<SyntaxInfo>();
\r
627 string str = this[row];
\r
628 int level = this.Hilighter.DoHilight(str, str.Length, (s) =>
\r
630 if (s.type == TokenType.None || s.type == TokenType.Control)
\r
632 if (str[s.index + s.length - 1] == Document.NewLine)
\r
634 syntax.Add(new SyntaxInfo(s.index, s.length, s.type));
\r
637 LineToIndexTableData lineData = this.GetData(row);
\r
638 lineData.Syntax = syntax.ToArray();
\r
640 if (level > 0 && hasBeginEncloser == false) //開始エンクロージャー
\r
642 lineData.EncloserType = EncloserType.Begin;
\r
643 hasBeginEncloser = true;
\r
645 else if (level < 0) //終了エンクロージャー
\r
647 lineData.EncloserType = EncloserType.End;
\r
648 hasBeginEncloser = false;
\r
650 else if (hasBeginEncloser) //エンクロージャーの範囲内
\r
651 lineData.EncloserType = EncloserType.Now;
\r
653 lineData.EncloserType = EncloserType.None;
\r
658 private int SearchStartRow(int startRow)
\r
660 for (startRow--; startRow >= 0; startRow--)
\r
662 EncloserType type = this.Lines[startRow].EncloserType;
\r
663 if (type == EncloserType.Begin || type == EncloserType.None)
\r
669 private int SearchEndRow(int startRow)
\r
671 for (startRow++ ; startRow < this.Lines.Count; startRow++)
\r
673 EncloserType type = this.Lines[startRow].EncloserType;
\r
674 if (type == EncloserType.End)
\r
677 return this.Lines.Count - 1;
\r
680 void SetUrlMarker(int row, int count)
\r
682 if (this.UrlMark == false)
\r
685 int startRow = row;
\r
686 int endRow = row + count - 1;
\r
688 for (int i = startRow; i <= endRow; i++)
\r
690 Match m = this.urlPattern.Match(this[i]);
\r
694 int lineHeadIndex = this.GetIndexFromLineNumber(i);
\r
695 int start = lineHeadIndex + m.Index;
\r
696 this.Document.RemoveMarker(start, 1);
\r
697 this.Document.Markers.Add(Marker.Create(start, m.Length, HilightType.Url));
\r
702 #region IEnumerable<string> メンバー
\r
705 /// コレクションを反復処理するためのIEnumeratorを返す
\r
707 /// <returns>IEnumeratorオブジェクト</returns>
\r
708 public IEnumerator<string> GetEnumerator()
\r
710 for (int i = 0; i < this.Lines.Count; i++)
\r
711 yield return this[i];
\r
716 #region IEnumerable メンバー
\r
718 System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
\r
720 for (int i = 0; i < this.Lines.Count; i++)
\r
721 yield return this[i];
\r