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