Free cookie consent management tool by TermsFeed Policy Generator

source: trunk/HeuristicLab.ExtLibs/HeuristicLab.NRefactory/5.5.0/NRefactory.CSharp-5.5.0/Refactoring/Script.cs @ 17197

Last change on this file since 17197 was 11700, checked in by jkarder, 10 years ago

#2077: created branch and added first version

File size: 20.3 KB
Line 
1//
2// Script.cs
3//
4// Author:
5//       Mike Krüger <mkrueger@novell.com>
6//
7// Copyright (c) 2011 Mike Krüger <mkrueger@novell.com>
8//
9// Permission is hereby granted, free of charge, to any person obtaining a copy
10// of this software and associated documentation files (the "Software"), to deal
11// in the Software without restriction, including without limitation the rights
12// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13// copies of the Software, and to permit persons to whom the Software is
14// furnished to do so, subject to the following conditions:
15//
16// The above copyright notice and this permission notice shall be included in
17// all copies or substantial portions of the Software.
18//
19// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25// THE SOFTWARE.
26using System;
27using System.Collections.Generic;
28using System.Diagnostics;
29using System.IO;
30using ICSharpCode.NRefactory.Editor;
31using ICSharpCode.NRefactory.TypeSystem;
32using System.Threading.Tasks;
33using System.Linq;
34using System.Text;
35using Mono.CSharp;
36using ITypeDefinition = ICSharpCode.NRefactory.TypeSystem.ITypeDefinition;
37
38namespace ICSharpCode.NRefactory.CSharp.Refactoring
39{
40  /// <summary>
41  /// Class for creating change scripts.
42  /// 'Original document' = document without the change script applied.
43  /// 'Current document' = document with the change script (as far as it is already created) applies.
44  /// </summary>
45  public abstract class Script : IDisposable
46  {
47    internal struct Segment : ISegment
48    {
49      readonly int offset;
50      readonly int length;
51     
52      public int Offset {
53        get { return offset; }
54      }
55     
56      public int Length {
57        get { return length; }
58      }
59     
60      public int EndOffset {
61        get { return Offset + Length; }
62      }
63     
64      public Segment (int offset, int length)
65      {
66        this.offset = offset;
67        this.length = length;
68      }
69     
70      public override string ToString ()
71      {
72        return string.Format ("[Script.Segment: Offset={0}, Length={1}, EndOffset={2}]", Offset, Length, EndOffset);
73      }
74    }
75   
76    readonly CSharpFormattingOptions formattingOptions;
77    readonly TextEditorOptions options;
78    readonly Dictionary<AstNode, ISegment> segmentsForInsertedNodes = new Dictionary<AstNode, ISegment>();
79   
80    protected Script(CSharpFormattingOptions formattingOptions, TextEditorOptions options)
81    {
82      if (formattingOptions == null)
83        throw new ArgumentNullException("formattingOptions");
84      if (options == null)
85        throw new ArgumentNullException("options");
86      this.formattingOptions = formattingOptions;
87      this.options = options;
88    }
89   
90    /// <summary>
91    /// Given an offset in the original document (at the start of script execution),
92    /// returns the offset in the current document.
93    /// </summary>
94    public abstract int GetCurrentOffset(int originalDocumentOffset);
95   
96    /// <summary>
97    /// Given an offset in the original document (at the start of script execution),
98    /// returns the offset in the current document.
99    /// </summary>
100    public abstract int GetCurrentOffset(TextLocation originalDocumentLocation);
101   
102    /// <summary>
103    /// Creates a tracked segment for the specified (offset,length)-segment.
104    /// Offset is interpreted to be an offset in the current document.
105    /// </summary>
106    /// <returns>
107    /// A segment that initially has the specified values, and updates
108    /// on every <see cref="Replace(int,int,string)"/> call.
109    /// </returns>
110    protected abstract ISegment CreateTrackedSegment(int offset, int length);
111
112    /// <summary>
113    /// Gets the current text segment of the specified AstNode.
114    /// </summary>
115    /// <param name="node">The node to get the segment for.</param>
116    public ISegment GetSegment(AstNode node)
117    {
118      ISegment segment;
119      if (segmentsForInsertedNodes.TryGetValue(node, out segment))
120        return segment;
121      if (node.StartLocation.IsEmpty || node.EndLocation.IsEmpty) {
122        throw new InvalidOperationException("Trying to get the position of a node that is not part of the original document and was not inserted");
123      }
124      int startOffset = GetCurrentOffset(node.StartLocation);
125      int endOffset = GetCurrentOffset(node.EndLocation);
126      return new Segment(startOffset, endOffset - startOffset);
127    }
128   
129    /// <summary>
130    /// Replaces text.
131    /// </summary>
132    /// <param name="offset">The starting offset of the text to be replaced.</param>
133    /// <param name="length">The length of the text to be replaced.</param>
134    /// <param name="newText">The new text.</param>
135    public abstract void Replace (int offset, int length, string newText);
136   
137    public void InsertText(int offset, string newText)
138    {
139      Replace(offset, 0, newText);
140    }
141   
142    public void RemoveText(int offset, int length)
143    {
144      Replace(offset, length, "");
145    }
146   
147    public CSharpFormattingOptions FormattingOptions {
148      get { return formattingOptions; }
149    }
150   
151    public TextEditorOptions Options {
152      get { return options; }
153    }
154   
155    public void InsertBefore(AstNode node, AstNode newNode)
156    {
157      var startOffset = GetCurrentOffset(new TextLocation(node.StartLocation.Line, 1));
158      var output = OutputNode (GetIndentLevelAt (startOffset), newNode);
159      string text = output.Text;
160      if (!(newNode is Expression || newNode is AstType))
161        text += Options.EolMarker;
162      InsertText(startOffset, text);
163      output.RegisterTrackedSegments(this, startOffset);
164      CorrectFormatting (node, newNode);
165    }
166
167    public void InsertAfter(AstNode node, AstNode newNode)
168    {
169            var indentLevel = IndentLevelFor(node);
170            var output = OutputNode(indentLevel, newNode);
171            string text =  PrefixFor(node, newNode) + output.Text;
172
173            var insertOffset = GetCurrentOffset(node.EndLocation);
174            InsertText(insertOffset, text);
175            output.RegisterTrackedSegments(this, insertOffset);
176            CorrectFormatting (node, newNode);
177    }
178
179      private int IndentLevelFor(AstNode node)
180      {
181            if (!DoesInsertingAfterRequireNewline(node))
182              return 0;
183         
184            return GetIndentLevelAt(GetCurrentOffset(new TextLocation(node.StartLocation.Line, 1)));
185      }
186
187      bool DoesInsertingAfterRequireNewline(AstNode node)
188      {
189            if (node is Expression)
190                return false;
191
192            if (node is AstType)
193                return false;
194
195          if (node is ParameterDeclaration)
196              return false;
197
198          var token = node as CSharpTokenNode;
199          if (token != null && token.Role == Roles.LPar)
200              return false;
201         
202          return true;
203      }
204
205      private string PrefixFor(AstNode node, AstNode newNode)
206      {
207          if (DoesInsertingAfterRequireNewline(node))
208              return Options.EolMarker;
209
210          if (newNode is ParameterDeclaration && node is ParameterDeclaration)
211              //todo: worry about adding characters to the document without matching AstNode's.
212              return ", ";
213
214          return String.Empty;
215      }
216
217      public void AddTo(BlockStatement bodyStatement, AstNode newNode)
218    {
219      var startOffset = GetCurrentOffset(bodyStatement.LBraceToken.EndLocation);
220      var output = OutputNode(1 + GetIndentLevelAt(startOffset), newNode, true);
221      InsertText(startOffset, output.Text);
222      output.RegisterTrackedSegments(this, startOffset);
223      CorrectFormatting (null, newNode);
224    }
225 
226    public void AddTo(TypeDeclaration typeDecl, EntityDeclaration entityDecl)
227    {
228      var startOffset = GetCurrentOffset(typeDecl.LBraceToken.EndLocation);
229      var output = OutputNode(1 + GetIndentLevelAt(startOffset), entityDecl, true);
230      InsertText(startOffset, output.Text);
231      output.RegisterTrackedSegments(this, startOffset);
232      CorrectFormatting (null, entityDecl);
233    }
234   
235    /// <summary>
236    /// Changes the modifier of a given entity declaration.
237    /// </summary>
238    /// <param name="entity">The entity.</param>
239    /// <param name="modifiers">The new modifiers.</param>
240    public void ChangeModifier(EntityDeclaration entity, Modifiers modifiers)
241    {
242      var dummyEntity = new MethodDeclaration ();
243      dummyEntity.Modifiers = modifiers;
244
245      int offset;
246      int endOffset;
247
248      if (entity.ModifierTokens.Any ()) {
249        offset = GetCurrentOffset(entity.ModifierTokens.First ().StartLocation);
250        endOffset = GetCurrentOffset(entity.ModifierTokens.Last ().GetNextSibling (s => s.Role != Roles.NewLine && s.Role != Roles.Whitespace).StartLocation);
251      } else {
252        var child = entity.FirstChild;
253        while (child.NodeType == NodeType.Whitespace ||
254               child.Role == EntityDeclaration.AttributeRole ||
255               child.Role == Roles.NewLine) {
256          child = child.NextSibling;
257        }
258        offset = endOffset = GetCurrentOffset(child.StartLocation);
259      }
260
261      var sb = new StringBuilder();
262      foreach (var modifier in dummyEntity.ModifierTokens) {
263        sb.Append(modifier.ToString());
264        sb.Append(' ');
265      }
266
267      Replace(offset, endOffset - offset, sb.ToString());
268    }
269
270    public void ChangeModifier(ParameterDeclaration param, ParameterModifier modifier)
271    {
272      var child = param.FirstChild;
273      Func<AstNode, bool> pred = s => s.Role == ParameterDeclaration.RefModifierRole || s.Role == ParameterDeclaration.OutModifierRole || s.Role == ParameterDeclaration.ParamsModifierRole || s.Role == ParameterDeclaration.ThisModifierRole;
274      if (!pred(child))
275        child = child.GetNextSibling(pred);
276
277      int offset;
278      int endOffset;
279
280      if (child != null) {
281        offset = GetCurrentOffset(child.StartLocation);
282        endOffset = GetCurrentOffset(child.GetNextSibling (s => s.Role != Roles.NewLine && s.Role != Roles.Whitespace).StartLocation);
283      } else {
284        offset = endOffset = GetCurrentOffset(param.Type.StartLocation);
285      }
286      string modString;
287      switch (modifier) {
288        case ParameterModifier.None:
289          modString = "";
290          break;
291        case ParameterModifier.Ref:
292          modString = "ref ";
293          break;
294        case ParameterModifier.Out:
295          modString = "out ";
296          break;
297        case ParameterModifier.Params:
298          modString = "params ";
299          break;
300        case ParameterModifier.This:
301          modString = "this ";
302          break;
303        default:
304          throw new ArgumentOutOfRangeException();
305      }
306      Replace(offset, endOffset - offset, modString);
307    }
308
309    /// <summary>
310    /// Changes the base types of a type declaration.
311    /// </summary>
312    /// <param name="type">The type declaration to modify.</param>
313    /// <param name="baseTypes">The new base types.</param>
314    public void ChangeBaseTypes(TypeDeclaration type, IEnumerable<AstType> baseTypes)
315    {
316      var dummyType = new TypeDeclaration();
317      dummyType.BaseTypes.AddRange(baseTypes);
318
319      int offset;
320      int endOffset;
321      var sb = new StringBuilder();
322
323      if (type.BaseTypes.Any ()) {
324        offset = GetCurrentOffset(type.ColonToken.StartLocation);
325        endOffset = GetCurrentOffset(type.BaseTypes.Last ().EndLocation);
326      } else {
327        sb.Append(' ');
328        if (type.TypeParameters.Any()) {
329          offset = endOffset = GetCurrentOffset(type.RChevronToken.EndLocation);
330        } else {
331          offset = endOffset = GetCurrentOffset(type.NameToken.EndLocation);
332        }
333      }
334
335      if (dummyType.BaseTypes.Any()) {
336        sb.Append(": ");
337        sb.Append(string.Join(", ", dummyType.BaseTypes));
338      }
339
340      Replace(offset, endOffset - offset, sb.ToString());
341      FormatText(type);
342    }
343
344
345    /// <summary>
346    /// Adds an attribute section to a given entity.
347    /// </summary>
348    /// <param name="entity">The entity to add the attribute to.</param>
349    /// <param name="attr">The attribute to add.</param>
350    public void AddAttribute(EntityDeclaration entity, AttributeSection attr)
351    {
352      var node = entity.FirstChild;
353      while (node.NodeType == NodeType.Whitespace || node.Role == Roles.Attribute) {
354        node = node.NextSibling;
355      }
356      InsertBefore(node, attr);
357    }
358
359    public virtual Task Link (params AstNode[] nodes)
360    {
361      // Default implementation: do nothing
362      // Derived classes are supposed to enter the text editor's linked state.
363     
364      // Immediately signal the task as completed:
365      var tcs = new TaskCompletionSource<object>();
366      tcs.SetResult(null);
367      return tcs.Task;
368    }
369
370    public virtual Task Link (IEnumerable<AstNode> nodes)
371    {
372      return Link(nodes.ToArray());
373    }
374   
375    public void Replace (AstNode node, AstNode replaceWith)
376    {
377      var segment = GetSegment (node);
378      int startOffset = segment.Offset;
379      int level = 0;
380      if (!(replaceWith is Expression) && !(replaceWith is AstType))
381        level = GetIndentLevelAt (startOffset);
382      NodeOutput output = OutputNode (level, replaceWith);
383      output.TrimStart ();
384      Replace (startOffset, segment.Length, output.Text);
385      output.RegisterTrackedSegments(this, startOffset);
386      CorrectFormatting (node, node);
387    }
388
389    List<AstNode> nodesToFormat = new List<AstNode> ();
390
391    void CorrectFormatting(AstNode node, AstNode newNode)
392    {
393      if (node is Identifier || node is IdentifierExpression || node is CSharpTokenNode || node is AstType)
394        return;
395      if (node == null || node.Parent is BlockStatement) {
396        nodesToFormat.Add (newNode);
397      } else {
398        nodesToFormat.Add ((node.Parent != null && (node.Parent is Statement || node.Parent is Expression || node.Parent is VariableInitializer)) ? node.Parent : newNode);
399      }
400    }
401   
402    public abstract void Remove (AstNode node, bool removeEmptyLine = true);
403
404    /// <summary>
405    /// Safely removes an attribue from it's section (removes empty sections).
406    /// </summary>
407    /// <param name="attr">The attribute to be removed.</param>
408    public void RemoveAttribute(Attribute attr)
409    {
410      AttributeSection section = (AttributeSection)attr.Parent;
411      if (section.Attributes.Count == 1) {
412        Remove(section);
413        return;
414      }
415
416      var newSection = (AttributeSection)section.Clone();
417      int i = 0;
418      foreach (var a in section.Attributes) {
419        if (a == attr)
420          break;
421        i++;
422      }
423      newSection.Attributes.Remove (newSection.Attributes.ElementAt (i));
424      Replace(section, newSection);
425    }
426   
427    public abstract void FormatText (IEnumerable<AstNode> nodes);
428
429    public void FormatText (params AstNode[] nodes)
430    {
431      FormatText ((IEnumerable<AstNode>)nodes);
432    }
433
434    public virtual void Select (AstNode node)
435    {
436      // default implementation: do nothing
437      // Derived classes are supposed to set the text editor's selection
438    }
439
440    public virtual void Select (TextLocation start, TextLocation end)
441    {
442      // default implementation: do nothing
443      // Derived classes are supposed to set the text editor's selection
444    }
445
446    public virtual void Select (int startOffset, int endOffset)
447    {
448      // default implementation: do nothing
449      // Derived classes are supposed to set the text editor's selection
450    }
451
452   
453    public enum InsertPosition
454    {
455      Start,
456      Before,
457      After,
458      End
459    }
460   
461    public virtual Task<Script> InsertWithCursor(string operation, InsertPosition defaultPosition, IList<AstNode> nodes)
462    {
463      throw new NotImplementedException();
464    }
465   
466    public virtual Task<Script> InsertWithCursor(string operation, ITypeDefinition parentType, Func<Script, RefactoringContext, IList<AstNode>> nodeCallback)
467    {
468      throw new NotImplementedException();
469    }
470   
471    public Task<Script> InsertWithCursor(string operation, InsertPosition defaultPosition, params AstNode[] nodes)
472    {
473      return InsertWithCursor(operation, defaultPosition, (IList<AstNode>)nodes);
474    }
475
476    public Task<Script> InsertWithCursor(string operation, ITypeDefinition parentType, Func<Script, RefactoringContext, AstNode> nodeCallback)
477    {
478      return InsertWithCursor(operation, parentType, (Func<Script, RefactoringContext, IList<AstNode>>)delegate (Script s, RefactoringContext ctx) {
479        return new AstNode[] { nodeCallback(s, ctx) };
480      });
481    }
482   
483    protected virtual int GetIndentLevelAt (int offset)
484    {
485      return 0;
486    }
487   
488    sealed class SegmentTrackingTokenWriter : TextWriterTokenWriter
489    {
490      internal List<KeyValuePair<AstNode, Segment>> NewSegments = new List<KeyValuePair<AstNode, Segment>>();
491      readonly Stack<int> startOffsets = new Stack<int>();
492      readonly StringWriter stringWriter;
493     
494      public SegmentTrackingTokenWriter(StringWriter stringWriter)
495        : base(stringWriter)
496      {
497        this.stringWriter = stringWriter;
498      }
499     
500      public override void WriteIdentifier (Identifier identifier)
501      {
502        int startOffset = stringWriter.GetStringBuilder ().Length;
503        int endOffset = startOffset + (identifier.Name ?? "").Length + (identifier.IsVerbatim ? 1 : 0);
504        NewSegments.Add(new KeyValuePair<AstNode, Segment>(identifier, new Segment(startOffset, endOffset - startOffset)));
505        base.WriteIdentifier (identifier);
506      }
507     
508      public override void StartNode (AstNode node)
509      {
510        base.StartNode (node);
511        startOffsets.Push(stringWriter.GetStringBuilder ().Length);
512      }
513     
514      public override void EndNode (AstNode node)
515      {
516        int startOffset = startOffsets.Pop();
517        int endOffset = stringWriter.GetStringBuilder ().Length;
518        NewSegments.Add(new KeyValuePair<AstNode, Segment>(node, new Segment(startOffset, endOffset - startOffset)));
519        base.EndNode (node);
520      }
521    }
522   
523    protected NodeOutput OutputNode(int indentLevel, AstNode node, bool startWithNewLine = false)
524    {
525      var stringWriter = new StringWriter ();
526      var formatter = new SegmentTrackingTokenWriter(stringWriter);
527      formatter.Indentation = indentLevel;
528      formatter.IndentationString = Options.TabsToSpaces ? new string (' ', Options.IndentSize) : "\t";
529      stringWriter.NewLine = Options.EolMarker;
530      if (startWithNewLine)
531        formatter.NewLine ();
532      var visitor = new CSharpOutputVisitor (formatter, formattingOptions);
533      node.AcceptVisitor (visitor);
534      string text = stringWriter.ToString().TrimEnd();
535      return new NodeOutput(text, formatter.NewSegments);
536    }
537   
538    protected class NodeOutput
539    {
540      string text;
541      readonly List<KeyValuePair<AstNode, Segment>> newSegments;
542      int trimmedLength;
543     
544      internal NodeOutput(string text, List<KeyValuePair<AstNode, Segment>> newSegments)
545      {
546        this.text = text;
547        this.newSegments = newSegments;
548      }
549     
550      public string Text {
551        get { return text; }
552      }
553     
554      public void TrimStart()
555      {
556        for (int i = 0; i < text.Length; i++) {
557          char ch = text [i];
558          if (ch != ' ' && ch != '\t') {
559            if (i > 0) {
560              text = text.Substring (i);
561              trimmedLength = i;
562            }
563            break;
564          }
565        }
566      }
567     
568      public void RegisterTrackedSegments(Script script, int insertionOffset)
569      {
570        foreach (var pair in newSegments) {
571          int offset = insertionOffset + pair.Value.Offset - trimmedLength;
572          ISegment trackedSegment = script.CreateTrackedSegment(offset, pair.Value.Length);
573          script.segmentsForInsertedNodes.Add(pair.Key, trackedSegment);
574        }
575      }
576    }
577   
578    /// <summary>
579    /// Renames the specified symbol.
580    /// </summary>
581    /// <param name='symbol'>
582    /// The symbol to rename
583    /// </param>
584    /// <param name='name'>
585    /// The new name, if null the user is prompted for a new name.
586    /// </param>
587    public virtual void Rename(ISymbol symbol, string name = null)
588    {
589    }
590   
591    public virtual void DoGlobalOperationOn(IEnumerable<IEntity> entities, Action<RefactoringContext, Script, IEnumerable<AstNode>> callback, string operationDescription = null)
592    {
593    }
594
595    public virtual void Dispose()
596    {
597      FormatText (nodesToFormat);
598    }
599   
600    public enum NewTypeContext {
601      /// <summary>
602      /// The class should be placed in a new file to the current namespace.
603      /// </summary>
604      CurrentNamespace,
605     
606      /// <summary>
607      /// The class should be placed in the unit tests. (not implemented atm.)
608      /// </summary>
609      UnitTests
610    }
611   
612    /// <summary>
613    /// Creates a new file containing the type, namespace and correct usings.
614    /// (Note: Should take care of IDE specific things, file headers, add to project, correct name).
615    /// </summary>
616    /// <param name='newType'>
617    /// New type to be created.
618    /// </param>
619    /// <param name='context'>
620    /// The Context in which the new type should be created.
621    /// </param>
622    public virtual void CreateNewType(AstNode newType, NewTypeContext context = NewTypeContext.CurrentNamespace)
623    {
624    }
625  }
626
627  public static class ExtMethods
628  {
629    public static void ContinueScript (this Task task, Action act)
630    {
631      if (task.IsCompleted) {
632        act();
633      } else {
634        task.ContinueWith(delegate {
635          act();
636        }, TaskScheduler.FromCurrentSynchronizationContext());
637      }
638    }
639
640    public static void ContinueScript (this Task<Script> task, Action<Script> act)
641    {
642      if (task.IsCompleted) {
643        act(task.Result);
644      } else {
645        task.ContinueWith(delegate {
646          act(task.Result);
647        }, TaskScheduler.FromCurrentSynchronizationContext());
648      }
649    }
650  }
651}
Note: See TracBrowser for help on using the repository browser.