Free cookie consent management tool by TermsFeed Policy Generator

source: branches/GBT/HeuristicLab.CodeEditor/3.4/CodeEditor.cs @ 12495

Last change on this file since 12495 was 12495, checked in by gkronber, 9 years ago

#2261: merged trunk changes to branch
r12494
#2403: added a null check in the MatlabParameterVectorEvaluator to prevent exceptions when clearstate is called


r12493
#2369: added support for squared errors and relative errors to error-characteristic-curve view


r12492
#2392: implemented PearsonsRCalculator to fix incorrect correlation values in the correlation matrix.


r12491
#2402 don't set task state to waiting when it fails


r12490
#2401 added missing Mono.Cecil plugin dependency


r12488
#2400 - Interfaces for Capaciated-, PickupAndDelivery- and TimeWindowed-ProblemInstances now specify an additional penalty parameter to set the current penalty factor for the constraint relaxation. - The setter of the penalty-property in ...


r12485
#2374 made RegressionSolution and ClassificationSolution non-abstract


r12482
#2320: Fixed warnings in unit test solutions introduced in r12420 by marking methods as obsolete.


r12481
#2320: Fixed AfterDeserialization of GEArtifialAntEvaluator.


r12480
#2320: Fixed error in symbolicexpressiontree crossover regarding the wiring of lookup parameters if persisted file is loaded.


r12479
#2397 moved GeoIP project into ExtLibs


r12478
#2329 fixed bug in simple code editor


r12476
#2331 removed outdated plugins


r12475
#2368 fixed compile warnings


r12474
#2399 worked on Mono project prepare script


r12473
#2329 added a simple code editor for Linux


r12472
#2399 - fixed MathJax.js file name - worked on Mono project prepare script


r12471
#2399 worked on Mono project prepare script


r12470
#2399 fixed pre-build events in project files


r12465
#2399 worked on mono project prepare script


r12464
#2399 added patch to project


r12463
#2399 fixed EPPlus so that it compiles on Linux


r12461
#2398: Skip root and start symbols when calculating impacts and replacement values in the pruning operators.


r12458
#2354 show label when no data is displayed and don't show the legend


r12457
#2353 removed duplicated call to Any() in Hive Status page


r12456
#2368 fixed modifier


r12455
#2368 added support in persistence for typecaches in streams


r12445
#2394: Changed Web.config compilation from debug to release to force script bundling. Changed history loading type from lazy to eager loading to increase performance. Fixed "getCoreStatus" typo in statusCtrl.js


r12443
#2394: Fixed UserTaskQuery and GetStatusHistory in the WebApp.Status plugin


r12442
#2394 added nuget folders to svn ignore list


r12435
#2394: Improved PluginManager and updated hive status monitor.


r12434
#2396 added symbolic expression tree formatter for C#


r12433
#2395: Minor change in DoubleValue.GetValue.


r12432
#2395 Use simple round-trip format for doubles because G17 prints some strange numbers (20.22 to 20.219999999999999999). Some accuracy can still be lost on 64bit machines, but should be very rare and minimal. double.MaxValue can still be pa...


r12431
#2395 Fixed parsing issues by using the G17 format.


r12430
#2394 added missing package config


r12429
#2394 added missing package config


r12428
#2394 added web app and status page to trunk


r12424
#2320: Adapted plugin file and updated project file of SymbolicExpressionTreeEncoding.


r12422
#2320: Merged the encoding class and all accompanying changes in the trunk.


r12401
#2387 Fixed a bug where the automatic selection of the first element behaved differently for the NewItemDialog.


r12400
#2387 Forgot to commit a file.


r12399
#2387 - Added context-menu for expanding and collapsing tree-nodes. - Improve response time when expanding/collapsing all nodes for TypeSelector and NewItemDialog.


r12398
#2387 - Added clearSearch-button in TypeSelector. - Adapted behavior of TypeSelector and NewItemDialog that a selected node stays selected as long as it matches the search criteria.


r12397
#2387 - Adapted behavior of the matching in the TypeSelector that it behave the same as the NewItemDialog. The search string is tokenized by space and matches if all tokens are contained, (eg. "Sym Reg" matches "SymbolicRegression...")...


r12393
#2025 - Removed Expand/CollapseAll buttons. - Removed cycling of items.


r12392
#2386: Updated GetHashCode method in the EnumerableBoolEqualityComparer.


File size: 18.8 KB
Line 
1#region License Information
2/* HeuristicLab
3 * Copyright (C) 2002-2015 Heuristic and Evolutionary Algorithms Laboratory (HEAL)
4 *
5 * This file is part of HeuristicLab.
6 *
7 * HeuristicLab is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * HeuristicLab is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with HeuristicLab. If not, see <http://www.gnu.org/licenses/>.
19 */
20#endregion
21
22using System;
23using System.CodeDom.Compiler;
24using System.Collections.Generic;
25using System.ComponentModel;
26using System.Reflection;
27using System.Threading.Tasks;
28using System.Windows.Documents;
29using ICSharpCode.AvalonEdit;
30using ICSharpCode.AvalonEdit.AddIn;
31using ICSharpCode.AvalonEdit.CodeCompletion;
32using ICSharpCode.AvalonEdit.Document;
33using ICSharpCode.AvalonEdit.Editing;
34using ICSharpCode.AvalonEdit.Highlighting;
35using ICSharpCode.AvalonEdit.Indentation.CSharp;
36using ICSharpCode.AvalonEdit.Search;
37using ICSharpCode.NRefactory.Editor;
38using ICSharpCode.SharpDevelop.Editor;
39using Forms = System.Windows.Forms;
40using Input = System.Windows.Input;
41using Media = System.Windows.Media;
42
43namespace HeuristicLab.CodeEditor {
44  public partial class CodeEditor : CodeEditorBase {
45    private static readonly Media.Color WarningColor = Media.Colors.Blue;
46    private static readonly Media.Color ErrorColor = Media.Colors.Red;
47    private static readonly Media.Color ReadOnlyColor = Media.Colors.Moccasin;
48
49    private const string DefaultDocumentFileName = "Document";
50    private const string DefaultTextEditorSyntaxHighlighting = "C#";
51    private const string DefaultTextEditorFontFamily = "Consolas";
52    private const double DefaultTextEditorFontSize = 13.0;
53    private const bool DefaultTextEditorShowLineNumbers = true;
54    private const bool DefaultTextEditorShowSpaces = true;
55    private const bool DefaultTextEditorShowTabs = true;
56    private const bool DefaultTextEditorConvertTabsToSpaces = true;
57    private const bool DefaultTextEditorHighlightCurrentLine = true;
58    private const int DefaultTextEditorIndentationSize = 2;
59
60    private AssemblyLoader assemblyLoader;
61    private TextMarkerService textMarkerService;
62
63    #region Properties
64    internal TextEditor TextEditor { get { return avalonEditWrapper.TextEditor; } }
65    internal Input.RoutedCommand CompletionCommand;
66    internal CompletionWindow CompletionWindow;
67    internal OverloadInsightWindow OverloadInsightWindow;
68
69    private TextDocument Doc { get { return TextEditor.Document; } }
70
71    private ITextMarker prefixMarker;
72    private string prefix = string.Empty;
73    public override string Prefix {
74      get { return prefix; }
75      set {
76        if (value == null) value = string.Empty;
77        if (prefix == value) return;
78        if (prefixMarker != null) prefixMarker.Delete();
79        Doc.Remove(0, prefix.Length);
80        prefix = value;
81        if (value.Length > 0) {
82          Doc.Insert(0, prefix);
83          prefixMarker = textMarkerService.Create(0, prefix.Length);
84          prefixMarker.BackgroundColor = ReadOnlyColor;
85        }
86      }
87    }
88
89    private ITextMarker suffixMarker;
90    private string suffix = string.Empty;
91    public override string Suffix {
92      get { return suffix; }
93      set {
94        if (value == null) value = string.Empty;
95        if (suffix == value) return;
96        if (suffixMarker != null) suffixMarker.Delete();
97        Doc.Remove(Doc.TextLength - suffix.Length, suffix.Length);
98        suffix = value;
99        if (value.Length > 0) {
100          int offset = Doc.TextLength;
101          Doc.Insert(offset, suffix);
102          suffixMarker = textMarkerService.Create(offset, suffix.Length);
103          suffixMarker.BackgroundColor = ReadOnlyColor;
104        }
105      }
106    }
107
108    public override string UserCode {
109      get { return Doc.GetText(prefix.Length, Doc.TextLength - suffix.Length - prefix.Length); }
110      set {
111        if (Doc.Text == value) return;
112        Doc.Replace(prefix.Length, Doc.TextLength - suffix.Length - prefix.Length, value);
113      }
114    }
115
116    #region TextEditor
117    [DefaultValue(DefaultTextEditorSyntaxHighlighting)]
118    public string TextEditorSyntaxHighlighting {
119      get { return TextEditor.SyntaxHighlighting.Name; }
120      set {
121        TextEditor.SyntaxHighlighting = HighlightingManager.Instance.GetDefinition(value);
122        ApplyLanguageFeatures();
123      }
124    }
125
126    [DefaultValue(DefaultTextEditorShowLineNumbers)]
127    public bool TextEditorShowLineNumbers {
128      get { return TextEditor.ShowLineNumbers; }
129      set { TextEditor.ShowLineNumbers = value; }
130    }
131
132    [DefaultValue(DefaultTextEditorShowSpaces)]
133    public bool TextEditorShowSpaces {
134      get { return TextEditor.Options.ShowSpaces; }
135      set { TextEditor.Options.ShowSpaces = value; }
136    }
137
138    [DefaultValue(DefaultTextEditorShowTabs)]
139    public bool TextEditorShowTabs {
140      get { return TextEditor.Options.ShowTabs; }
141      set { TextEditor.Options.ShowTabs = value; }
142    }
143
144    [DefaultValue(DefaultTextEditorConvertTabsToSpaces)]
145    public bool TextEditorConvertTabsToSpaces {
146      get { return TextEditor.Options.ConvertTabsToSpaces; }
147      set { TextEditor.Options.ConvertTabsToSpaces = value; }
148    }
149
150    [DefaultValue(DefaultTextEditorHighlightCurrentLine)]
151    public bool TextEditorHighlightCurrentLine {
152      get { return TextEditor.Options.HighlightCurrentLine; }
153      set { TextEditor.Options.HighlightCurrentLine = value; }
154    }
155
156    [DefaultValue(DefaultTextEditorIndentationSize)]
157    public int TextEditorIndentationSize {
158      get { return TextEditor.Options.IndentationSize; }
159      set { TextEditor.Options.IndentationSize = value; }
160    }
161    #endregion
162
163    public override bool ReadOnly {
164      get { return TextEditor.IsReadOnly; }
165      set { TextEditor.IsReadOnly = value; }
166    }
167    #endregion
168
169    public CodeEditor() {
170      InitializeComponent();
171      InitializeTextEditor();
172    }
173
174    private void InitializeTextEditor() {
175      #region AssemblyLoader
176      assemblyLoader = new AssemblyLoader();
177      assemblyLoader.AssembliesLoading += (sender, args) => OnAssembliesLoading(args.Value);
178      assemblyLoader.InternalAssembliesLoaded += (sender, args) => OnInternalAssembliesLoaded(args.Value);
179      assemblyLoader.AssembliesLoaded += (sender, args) => OnAssembliesLoaded(args.Value);
180      assemblyLoader.AssembliesUnloading += (sender, args) => OnAssembliesUnloading(args.Value);
181      assemblyLoader.InternalAssembliesUnloaded += (sender, args) => OnInternalAssembliesUnloaded(args.Value);
182      assemblyLoader.AssembliesUnloaded += (sender, args) => OnAssembliesUnloaded(args.Value);
183      #endregion
184
185      #region TextMarkerService
186      textMarkerService = new TextMarkerService(TextEditor.Document);
187      TextEditor.TextArea.TextView.BackgroundRenderers.Add(textMarkerService);
188      TextEditor.TextArea.TextView.LineTransformers.Add(textMarkerService);
189      TextEditor.TextArea.TextView.Services.AddService(typeof(ITextMarkerService), textMarkerService);
190      #endregion
191
192      #region ReadOnlySectionProvider
193      TextEditor.TextArea.ReadOnlySectionProvider = new MethodDefinitionReadOnlySectionProvider(this);
194      #endregion
195
196      #region SearchPanel
197      SearchPanel.Install(TextEditor);
198      #endregion
199
200      #region CompletionCommand
201      CompletionCommand = new Input.RoutedCommand();
202      CompletionCommand.InputGestures.Add(new Input.KeyGesture(Input.Key.Space, Input.ModifierKeys.Control));
203      #endregion
204
205      #region MoveLinesUpCommand
206      var moveLinesUpCommand = new Input.RoutedCommand();
207      moveLinesUpCommand.InputGestures.Add(new Input.KeyGesture(Input.Key.Up, Input.ModifierKeys.Alt));
208      var moveLinesUpCommandBinding = new Input.CommandBinding(moveLinesUpCommand, (sender, args) => ExecuteMoveLinesCommand(MovementDirection.Up));
209      TextEditor.CommandBindings.Add(moveLinesUpCommandBinding);
210      #endregion
211
212      #region MoveLinesDownCommand
213      var moveLinesDownCommand = new Input.RoutedCommand();
214      moveLinesDownCommand.InputGestures.Add(new Input.KeyGesture(Input.Key.Down, Input.ModifierKeys.Alt));
215      var moveLinesDownCommandBinding = new Input.CommandBinding(moveLinesDownCommand, (sender, args) => ExecuteMoveLinesCommand(MovementDirection.Down));
216      TextEditor.CommandBindings.Add(moveLinesDownCommandBinding);
217      #endregion
218
219      #region GoToLineCommand
220      var goToLineCommand = new Input.RoutedCommand();
221      goToLineCommand.InputGestures.Add(new Input.KeyGesture(Input.Key.G, Input.ModifierKeys.Control));
222      var goToLineCommandBinding = new Input.CommandBinding(goToLineCommand, (sender, args) => ExecuteGoToLineCommand());
223      TextEditor.CommandBindings.Add(goToLineCommandBinding);
224      #endregion
225
226      TextEditorSyntaxHighlighting = DefaultTextEditorSyntaxHighlighting;
227      TextEditorShowLineNumbers = DefaultTextEditorShowLineNumbers;
228      TextEditorShowSpaces = DefaultTextEditorShowSpaces;
229      TextEditorShowTabs = DefaultTextEditorShowTabs;
230      TextEditorConvertTabsToSpaces = DefaultTextEditorConvertTabsToSpaces;
231      TextEditorHighlightCurrentLine = DefaultTextEditorHighlightCurrentLine;
232      TextEditorIndentationSize = DefaultTextEditorIndentationSize;
233
234      Doc.FileName = DefaultDocumentFileName;
235
236      TextEditor.FontFamily = new Media.FontFamily(DefaultTextEditorFontFamily);
237      TextEditor.FontSize = DefaultTextEditorFontSize;
238      TextEditor.Options.EnableVirtualSpace = true;
239      TextEditor.TextArea.IndentationStrategy = new CSharpIndentationStrategy(TextEditor.Options);
240
241      TextEditor.TextChanged += (sender, args) => {
242        foreach (var marker in textMarkerService.TextMarkers) {
243          if (marker == prefixMarker || marker == suffixMarker) continue;
244          if (marker.Length != (int)marker.Tag)
245            marker.Delete();
246          else {
247            int caretOffset = TextEditor.CaretOffset;
248            var line = Doc.GetLineByOffset(marker.StartOffset);
249            int lineEndOffset = line.EndOffset;
250            if (caretOffset == lineEndOffset) // special case for markers beyond line length
251              marker.Delete();
252          }
253        }
254        OnTextEditorTextChanged();
255      };
256    }
257
258    #region Assembly Management
259    public override void AddAssembly(Assembly a) {
260      assemblyLoader.AddAssembly(a);
261    }
262
263    public override void AddAssemblies(IEnumerable<Assembly> assemblies) {
264      assemblyLoader.AddAssemblies(assemblies);
265    }
266
267    public override async Task AddAssembliesAsync(IEnumerable<Assembly> assemblies) {
268      await assemblyLoader.AddAssembliesAsync(assemblies);
269    }
270
271    public override void RemoveAssembly(Assembly a) {
272      assemblyLoader.RemoveAssembly(a);
273    }
274    #endregion
275
276    public override void ScrollToPosition(int line, int column) {
277      var segment = GetSegmentAtLocation(line, column);
278      TextEditor.CaretOffset = segment.Offset + segment.Length;
279      TextEditor.ScrollToLine(line);
280    }
281
282    public override void ScrollAfterPrefix() {
283      var location = Doc.GetLocation(prefix.Length);
284      ScrollToPosition(location.Line, location.Column);
285    }
286
287    private enum MovementDirection { Up, Down }
288    private void ExecuteMoveLinesCommand(MovementDirection movementDirection) {
289      var textArea = TextEditor.TextArea;
290      var selection = textArea.Selection;
291      var caret = textArea.Caret;
292
293      var selectionStartPosition = selection.StartPosition;
294      var selectionEndPosition = selection.EndPosition;
295      var caretPosition = caret.Position;
296      int caretOffset = caret.Offset;
297
298      bool advancedPositionCalcualtion = selection is RectangleSelection || !selection.IsEmpty;
299
300      int selectionStartOffset, selectionEndOffset;
301      if (advancedPositionCalcualtion) {
302        if (selectionStartPosition.CompareTo(selectionEndPosition) > 0) {
303          var temp = selectionStartPosition;
304          selectionStartPosition = selectionEndPosition;
305          selectionEndPosition = temp;
306        }
307        selectionStartOffset = Doc.GetOffset(selectionStartPosition.Location);
308        selectionEndOffset = Doc.GetOffset(selectionEndPosition.Location);
309      } else {
310        selectionStartOffset = selectionEndOffset = TextEditor.SelectionStart;
311      }
312
313      int selectionLength = selection.Length;
314
315      var startLine = Doc.GetLineByOffset(selectionStartOffset);
316      var endLine = Doc.GetLineByOffset(selectionEndOffset);
317
318      if (selection.IsMultiline && selectionEndOffset == endLine.Offset) {
319        if (movementDirection == MovementDirection.Down && endLine.TotalLength == 0) return;
320        endLine = endLine.PreviousLine;
321      }
322
323      if (movementDirection == MovementDirection.Up && startLine.LineNumber == 1) return;
324      if (movementDirection == MovementDirection.Down && endLine.LineNumber == Doc.LineCount) return;
325
326      int startOffset = startLine.Offset;
327      string primaryText = Doc.GetText(startOffset, endLine.EndOffset - startOffset);
328      string primaryDelimiter = Doc.GetText(endLine.EndOffset, endLine.DelimiterLength);
329
330      var secondaryLine = movementDirection == MovementDirection.Up ? startLine.PreviousLine : endLine.NextLine;
331      string secondaryText = Doc.GetText(secondaryLine.Offset, secondaryLine.Length);
332      string secondaryDelimiter = Doc.GetText(secondaryLine.EndOffset, secondaryLine.DelimiterLength);
333
334      if (string.IsNullOrEmpty(primaryText + primaryDelimiter) || string.IsNullOrEmpty(secondaryText + secondaryDelimiter)) return;
335
336      if (movementDirection == MovementDirection.Up) {
337        string replacementText = primaryText + secondaryDelimiter + secondaryText + primaryDelimiter;
338        Doc.Replace(secondaryLine.Offset, replacementText.Length, replacementText);
339        int correctionLength = secondaryText.Length + secondaryDelimiter.Length;
340        selectionStartOffset -= correctionLength;
341        caretOffset -= correctionLength;
342      } else {
343        string replacementText = secondaryText + primaryDelimiter + primaryText + secondaryDelimiter;
344        Doc.Replace(startLine.Offset, replacementText.Length, replacementText);
345        int correctionLength = secondaryText.Length + primaryDelimiter.Length;
346        selectionStartOffset += correctionLength;
347        caretOffset += correctionLength;
348      }
349
350      if (advancedPositionCalcualtion) {
351        var newSelectionStartLocation = Doc.GetLocation(selectionStartOffset);
352        int selectionLineOffset = newSelectionStartLocation.Line - Math.Min(selectionStartPosition.Line, selectionEndPosition.Line);
353        selectionStartPosition.Line += selectionLineOffset;
354        selectionEndPosition.Line += selectionLineOffset;
355        if (selectionEndPosition.Line > Doc.LineCount) {
356          var newLocation = Doc.GetLocation(Doc.TextLength);
357          selectionEndPosition.Line = newLocation.Line;
358          selectionEndPosition.Column = newLocation.Column;
359          selectionEndPosition.VisualColumn = newLocation.Column - 1;
360          selectionEndPosition.IsAtEndOfLine = selectionStartPosition.IsAtEndOfLine; // actual value does not matter; needed for comparison
361        }
362
363        if (selectionStartPosition == selectionEndPosition)
364          textArea.ClearSelection();
365        else {
366          if (selection is RectangleSelection)
367            textArea.Selection = new RectangleSelection(textArea, selectionStartPosition, selectionEndPosition);
368          else
369            textArea.Selection = new SimpleSelection(textArea, selectionStartPosition, selectionEndPosition);
370        }
371      } else {
372        TextEditor.SelectionStart = selectionStartOffset;
373        TextEditor.SelectionLength = selectionLength;
374      }
375
376      var newCaretLocation = Doc.GetLocation(Math.Min(caretOffset, Doc.TextLength));
377      var newCaretPosition = new TextViewPosition(newCaretLocation);
378      if (caretPosition.VisualColumn > caretPosition.Column) {
379        newCaretPosition.VisualColumn = caretPosition.VisualColumn;
380      }
381      caret.Position = newCaretPosition;
382    }
383
384    private void ExecuteGoToLineCommand() {
385      using (var dlg = new GoToLineDialog(TextEditor)) {
386        var result = dlg.ShowDialog(this);
387        if (result == Forms.DialogResult.OK) {
388          int lineNumber = dlg.Line;
389          var line = Doc.GetLineByNumber(lineNumber);
390          int offset = line.Offset;
391          if (TextUtilities.GetLeadingWhitespace(Doc, line).Length > 0)
392            offset = TextUtilities.GetNextCaretPosition(Doc, offset, LogicalDirection.Forward, CaretPositioningMode.WordBorder);
393          TextEditor.CaretOffset = offset;
394        }
395      }
396    }
397
398    #region Compiler Errors
399    public override void ShowCompileErrors(CompilerErrorCollection compilerErrors) {
400      if (compilerErrors == null) return;
401
402      textMarkerService.RemoveAll(x => x != prefixMarker && x != suffixMarker);
403
404      foreach (CompilerError error in compilerErrors) {
405        var startLocation = Doc.GetLocation(prefix.Length);
406        if (error.Line == 1) error.Column += startLocation.Column;
407        error.Line += startLocation.Line;
408        AddErrorMarker(error);
409      }
410    }
411
412    private void AddErrorMarker(CompilerError error) {
413      var segment = GetSegmentAtLocation(error.Line, error.Column);
414      var marker = textMarkerService.Create(segment.Offset, segment.Length);
415      marker.MarkerTypes = TextMarkerTypes.SquigglyUnderline;
416      marker.MarkerColor = error.IsWarning ? WarningColor : ErrorColor;
417      marker.Tag = segment.Length;
418    }
419
420    private ISegment GetSegmentAtLocation(int line, int column) {
421      line = Math.Max(Doc.GetLocation(prefix.Length).Line, line - 1);
422      line = Math.Min(Doc.GetLocation(Doc.TextLength - suffix.Length).Line, line);
423
424      var startOffset = Doc.GetOffset(line, column);
425      var lineEndOffset = Doc.GetLineByNumber(line).EndOffset;
426      var endOffset = TextUtilities.GetNextCaretPosition(Doc, startOffset, LogicalDirection.Forward, CaretPositioningMode.WordBorder);
427      if (endOffset < 0) endOffset = startOffset;
428      endOffset = Math.Min(lineEndOffset + 1, endOffset);
429
430      var segment = new TextSegment { StartOffset = startOffset, EndOffset = endOffset };
431
432      return segment;
433    }
434    #endregion
435
436    private void ApplyLanguageFeatures() {
437      switch (TextEditorSyntaxHighlighting) {
438        case "XML":
439          XmlLanguageFeatures.Apply(this);
440          break;
441        default:
442          CSharpLanguageFeatures.Apply(this);
443          break;
444      }
445    }
446  }
447}
Note: See TracBrowser for help on using the repository browser.