PageRenderTime 40ms CodeModel.GetById 14ms app.highlight 20ms RepoModel.GetById 1ms app.codeStats 0ms

/NRefactory/ICSharpCode.NRefactory.CSharp/Refactoring/Script.cs

http://github.com/icsharpcode/ILSpy
C# | 651 lines | 454 code | 87 blank | 110 comment | 82 complexity | 507f984ee3b1096f09fbd89f1bfe69d0 MD5 | raw file
  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}