5
// Mike KrĆ¼ger <mkrueger@novell.com>
7
// Copyright (c) 2009 Novell, Inc (http://www.novell.com)
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:
16
// The above copyright notice and this permission notice shall be included in
17
// all copies or substantial portions of the Software.
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
29
using System.Collections.Generic;
32
using MonoDevelop.Ide.Gui;
33
using MonoDevelop.Projects.Dom;
34
using MonoDevelop.Projects.Dom.Parser;
35
using MonoDevelop.Core;
36
using Mono.TextEditor;
37
using MonoDevelop.Ide;
39
using Mono.TextEditor.PopupWindow;
40
using MonoDevelop.Refactoring;
41
using MonoDevelop.CSharp.Parser;
42
using MonoDevelop.CSharp.Ast;
43
using MonoDevelop.Projects.Text;
44
using MonoDevelop.Projects.Dom.Output;
45
using MonoDevelop.CSharp.Resolver;
46
using MonoDevelop.CSharp.Formatting;
48
namespace MonoDevelop.CSharp.Refactoring.ExtractMethod
50
public class ExtractMethodRefactoring : RefactoringOperation
52
public override string AccelKey {
54
var cmdInfo = IdeApp.CommandService.GetCommandInfo (RefactoryCommands.ExtractMethod);
55
if (cmdInfo != null && cmdInfo.AccelKey != null)
56
return cmdInfo.AccelKey.Replace ("dead_circumflex", "^");
61
public ExtractMethodRefactoring ()
63
Name = "Extract Method";
66
public override bool IsValid (RefactoringOptions options)
68
// if (options.SelectedItem != null)
70
var buffer = options.Document.Editor;
71
if (buffer.Document.MimeType != CSharpFormatter.MimeType)
73
if (buffer.IsSomethingSelected) {
74
ParsedDocument doc = options.ParseDocument ();
75
if (doc != null && doc.CompilationUnit != null) {
76
var member = doc.CompilationUnit.GetMemberAt (buffer.Caret.Line, buffer.Caret.Column);
79
if (!member.BodyRegion.Contains (buffer.Caret.Line, buffer.Caret.Column))
87
public override string GetMenuDescription (RefactoringOptions options)
89
return GettextCatalog.GetString ("_Extract Method...");
92
public override void Run (RefactoringOptions options)
94
ExtractMethodParameters param = CreateParameters (options);
97
if (!Analyze (options, param, false)) {
98
MessageService.ShowError (GettextCatalog.GetString ("Invalid selection for method extraction."));
101
MessageService.ShowCustomDialog (new ExtractMethodDialog (options, this, param));
104
public ExtractMethodParameters CreateParameters (RefactoringOptions options)
106
var buffer = options.Document.Editor;
108
if (!buffer.IsSomethingSelected)
111
ParsedDocument doc = options.ParseDocument ();
112
if (doc == null || doc.CompilationUnit == null)
115
IMember member = doc.CompilationUnit.GetMemberAt (buffer.Caret.Line, buffer.Caret.Column);
119
ExtractMethodParameters param = new ExtractMethodParameters () {
120
DeclaringMember = member,
121
Location = new DomLocation (buffer.Caret.Line, buffer.Caret.Column)
123
Analyze (options, param, true);
127
public class ExtractMethodParameters
129
public IMember DeclaringMember {
144
public bool GenerateComment {
149
public bool ReferencesMember {
154
public InsertionPoint InsertionPoint {
159
public DomLocation Location {
164
public Modifiers Modifiers {
169
public List<VariableDescriptor> Variables {
174
public List<AstNode> Nodes {
179
public int StartOffset {
184
public int EndOffset {
191
/// The type of the expression, if the text is an expression, otherwise null.
193
public IReturnType ExpressionType {
198
public bool OneChangedVariable {
203
List<VariableDescriptor> parameters = new List<VariableDescriptor> ();
204
public List<VariableDescriptor> Parameters {
211
static string GetIndent (string text)
213
Mono.TextEditor.Document doc = new Mono.TextEditor.Document ();
215
string result = null;
216
for (int i = 1; i < doc.LineCount; i++) {
217
string lineIndent = doc.GetLineIndent (i);
218
if (doc.GetLine (i).EditableLength == lineIndent.Length)
220
if (result == null || lineIndent.Length < result.Length)
226
static string RemoveIndent (string text, string indent)
228
Mono.TextEditor.Document doc = new Mono.TextEditor.Document ();
230
StringBuilder result = new StringBuilder ();
231
bool firstLine = true;
232
foreach (LineSegment line in doc.Lines) {
233
string curLineIndent = line.GetIndentation (doc);
234
if (firstLine && curLineIndent.Length == line.EditableLength)
237
int offset = Math.Min (curLineIndent.Length, indent.Length);
238
result.Append (doc.GetTextBetween (line.Offset + offset, line.EndOffset));
240
return result.ToString ();
243
static string AddIndent (string text, string indent)
245
Mono.TextEditor.Document doc = new Mono.TextEditor.Document ();
247
StringBuilder result = new StringBuilder ();
248
foreach (LineSegment line in doc.Lines) {
249
if (result.Length > 0)
250
result.Append (indent);
251
result.Append (doc.GetTextAt (line));
253
return result.ToString ();
256
bool Analyze (RefactoringOptions options, ExtractMethodParameters param, bool fillParameter)
258
var data = options.GetTextEditorData ();
259
var parser = new CSharpParser ();
260
var unit = parser.Parse (data);
261
var resolver = options.GetResolver ();
264
var selectionRange = data.SelectionRange;
265
var startOffset = selectionRange.Offset;
266
while (startOffset + 1 < data.Length && char.IsWhiteSpace (data.GetCharAt (startOffset + 1)))
268
var endOffset = selectionRange.EndOffset;
269
while (startOffset < endOffset && endOffset - 1 > 0 && char.IsWhiteSpace (data.GetCharAt (endOffset - 1)))
271
if (startOffset >= endOffset)
274
var endLocation = data.OffsetToLocation (endOffset);
275
var startLocation = data.OffsetToLocation (startOffset);
276
param.StartOffset = startOffset;
277
param.EndOffset = endOffset;
278
param.Nodes = new List<AstNode> (unit.GetNodesBetween (startLocation.Line, startLocation.Column, endLocation.Line, endLocation.Column));
280
string text = options.Document.Editor.GetTextBetween (startLocation, endLocation);
282
param.Text = RemoveIndent (text, GetIndent (data.GetTextBetween (data.GetLine (startLocation.Line).Offset, data.GetLine (endLocation.Line).EndOffset))).TrimEnd ('\n', '\r');
283
VariableLookupVisitor visitor = new VariableLookupVisitor (resolver, param.Location);
284
visitor.MemberLocation = param.DeclaringMember.Location;
285
visitor.CutRegion = new DomRegion (startLocation.Line, startLocation.Column, endLocation.Line, endLocation.Column);
287
unit.AcceptVisitor (visitor, null);
288
if (param.Nodes != null && (param.Nodes.Count == 1 && param.Nodes [0].NodeType == NodeType.Expression)) {
289
ResolveResult resolveResult = resolver.Resolve (new ExpressionResult ("(" + text + ")"), param.Location);
290
if (resolveResult != null && resolveResult.ResolvedType != null)
291
param.ExpressionType = resolveResult.ResolvedType;
294
foreach (VariableDescriptor varDescr in visitor.VariableList.Where (v => !v.IsDefinedInsideCutRegion && (v.UsedInCutRegion || v.IsChangedInsideCutRegion || v.UsedAfterCutRegion && v.IsDefinedInsideCutRegion))) {
295
param.Parameters.Add (varDescr);
298
param.Variables = new List<VariableDescriptor> (visitor.Variables.Values);
299
param.ReferencesMember = visitor.ReferencesMember;
301
param.OneChangedVariable = param.Variables.Count (p => p.IsDefinedInsideCutRegion && p.UsedAfterCutRegion) == 1;
302
if (param.OneChangedVariable)
303
param.ExpressionType = param.Variables.First (p => p.IsDefinedInsideCutRegion && p.UsedAfterCutRegion).ReturnType;
305
foreach (VariableDescriptor varDescr in visitor.VariableList.Where (v => !v.IsDefined && param.Variables.Contains (v))) {
306
if (param.Parameters.Contains (varDescr))
308
if (startLocation <= varDescr.Location && varDescr.Location < endLocation)
310
param.Parameters.Add (varDescr);
314
param.ChangedVariables = new HashSet<string> (visitor.Variables.Values.Where (v => v.GetsChanged).Select (v => v.Name));
316
// analyze the variables outside of the selected text
317
IMember member = param.DeclaringMember;
319
int bodyStartOffset = data.Document.LocationToOffset (member.BodyRegion.Start.Line, member.BodyRegion.Start.Column);
320
int bodyEndOffset = data.Document.LocationToOffset (member.BodyRegion.End.Line, member.BodyRegion.End.Column);
321
if (startOffset < bodyStartOffset || bodyEndOffset < endOffset)
323
text = data.Document.GetTextBetween (bodyStartOffset, startOffset) + data.Document.GetTextBetween (endOffset, bodyEndOffset);
324
// ICSharpCode.NRefactory.Ast.INode parsedNode = provider.ParseText (text);
325
// visitor = new VariableLookupVisitor (resolver, param.Location);
326
// visitor.CutRegion = new DomRegion (data.MainSelection.MinLine, data.MainSelection.MaxLine);
327
// visitor.MemberLocation = new Location (param.DeclaringMember.Location.Column, param.DeclaringMember.Location.Line);
328
// if (parsedNode != null)
329
// parsedNode.AcceptVisitor (visitor, null);
333
param.VariablesOutside = new Dictionary<string, VariableDescriptor> ();
334
foreach (var pair in visitor.Variables) {
335
if (startLocation < pair.Value.Location || endLocation >= pair.Value.Location) {
336
param.VariablesOutside.Add (pair.Key, pair.Value);
339
param.OutsideVariableList = new List<VariableDescriptor> ();
340
foreach (var v in visitor.VariableList) {
341
if (startLocation < v.Location || endLocation >= v.Location)
342
param.OutsideVariableList.Add (v);
345
param.ChangedVariablesUsedOutside = new List<VariableDescriptor> (param.Variables.Where (v => v.GetsChanged && param.VariablesOutside.ContainsKey (v.Name)));
346
param.OneChangedVariable = param.Nodes.Count == 1 && param.Nodes[0] is BlockStatement;
347
if (param.OneChangedVariable)
348
param.OneChangedVariable = param.ChangedVariablesUsedOutside.Count == 1;
350
param.VariablesToGenerate = new List<VariableDescriptor> (param.ChangedVariablesUsedOutside.Where (v => v.IsDefined));
351
foreach (VariableDescriptor var in param.VariablesToGenerate) {
352
param.Parameters.Add (var);
354
if (param.OneChangedVariable) {
355
param.VariablesToDefine = new List<VariableDescriptor> (param.Parameters.Where (var => !var.InitialValueUsed));
356
param.VariablesToDefine.ForEach (var => param.Parameters.Remove (var));
358
param.VariablesToDefine = new List<VariableDescriptor> ();
365
static string GenerateMethodCall (RefactoringOptions options, ExtractMethodParameters param)
367
// var data = options.GetTextEditorData ();
368
StringBuilder sb = new StringBuilder ();
370
/* LineSegment line = data.Document.GetLine (Math.Max (0, data.Document.OffsetToLineNumber (data.SelectionRange.Offset) - 1));
371
if (param.VariablesToGenerate != null && param.VariablesToGenerate.Count > 0) {
372
string indent = options.GetWhitespaces (line.Offset);
373
sb.Append (Environment.NewLine + indent);
374
foreach (VariableDescriptor var in param.VariablesToGenerate) {
375
var returnType = options.ShortenTypeName (var.ReturnType);
376
sb.Append (returnType.ToInvariantString ());
378
sb.Append (var.Name);
383
if (param.OneChangedVariable) {
384
var resultVariable = param.Variables.First (p => p.IsDefinedInsideCutRegion && p.UsedAfterCutRegion);
385
if (resultVariable.IsDefinedInsideCutRegion) {
386
var s = resultVariable.Declaration.Type.StartLocation;
387
var e = resultVariable.Declaration.Type.EndLocation;
388
sb.Append (options.Document.Editor.GetTextBetween (s.Line, s.Column, e.Line, e.Column) + " ");
391
sb.Append (resultVariable.Name);
394
sb.Append (param.Name);
395
sb.Append (" "); // TODO: respect formatting
398
foreach (VariableDescriptor var in param.Parameters) {
399
if (param.OneChangedVariable && var.UsedAfterCutRegion && !var.UsedInCutRegion)
404
sb.Append (", "); // TODO: respect formatting
406
if (!param.OneChangedVariable) {
407
if (!var.IsDefinedInsideCutRegion && var.IsChangedInsideCutRegion) {
408
sb.Append (var.UsedBeforeCutRegion ? "ref " : "out ");
411
sb.Append (var.Name);
414
if (param.Nodes != null && (param.Nodes.Count > 1 || param.Nodes.Count == 1 && param.Nodes[0].NodeType != NodeType.Expression))
416
return sb.ToString ();
419
static DomMethod GenerateMethodStub (RefactoringOptions options, ExtractMethodParameters param)
421
DomMethod result = new DomMethod ();
422
result.Name = param.Name;
423
result.ReturnType = param.ExpressionType ?? DomReturnType.Void;
424
result.Modifiers = param.Modifiers;
425
if (!param.ReferencesMember)
426
result.Modifiers |= Modifiers.Static;
428
if (param.Parameters == null)
430
foreach (var p in param.Parameters) {
431
if (param.OneChangedVariable && p.UsedAfterCutRegion && !p.UsedInCutRegion)
433
var newParameter = new DomParameter ();
434
newParameter.Name = p.Name;
435
newParameter.ReturnType = p.ReturnType;
437
if (!param.OneChangedVariable) {
438
if (!p.IsDefinedInsideCutRegion && p.IsChangedInsideCutRegion) {
439
newParameter.ParameterModifiers = p.UsedBeforeCutRegion ? ParameterModifiers.Ref : ParameterModifiers.Out;
442
result.Add (newParameter);
447
static string GenerateMethodDeclaration (RefactoringOptions options, ExtractMethodParameters param)
449
StringBuilder methodText = new StringBuilder ();
450
string indent = options.GetIndent (param.DeclaringMember);
451
if (param.InsertionPoint != null) {
452
switch (param.InsertionPoint.LineBefore) {
453
case NewLineInsertion.Eol:
454
methodText.AppendLine ();
456
case NewLineInsertion.BlankLine:
457
methodText.Append (indent);
458
methodText.AppendLine ();
462
methodText.AppendLine ();
463
methodText.Append (indent);
464
methodText.AppendLine ();
466
var codeGenerator = new CSharpCodeGenerator () {
467
UseSpaceIndent = options.Document.Editor.Options.TabsToSpaces,
468
EolMarker = options.Document.Editor.EolMarker,
469
TabSize = options.Document.Editor.Options.TabSize
472
var newMethod = GenerateMethodStub (options, param);
473
IType callingType = null;
474
var cu = options.Document.CompilationUnit;
476
callingType = newMethod.DeclaringType = options.Document.CompilationUnit.GetTypeAt (options.Document.Editor.Caret.Line, options.Document.Editor.Caret.Column);
478
var createdMethod = codeGenerator.CreateMemberImplementation (callingType, newMethod, false);
480
if (param.GenerateComment && DocGenerator.Instance != null)
481
methodText.AppendLine (DocGenerator.Instance.GenerateDocumentation (newMethod, indent + "/// "));
482
string code = createdMethod.Code;
483
int idx1 = code.LastIndexOf ("throw");
484
int idx2 = code.LastIndexOf (";");
485
methodText.Append (code.Substring (0, idx1));
487
if (param.Nodes != null && (param.Nodes.Count == 1 && param.Nodes[0].NodeType == NodeType.Expression)) {
488
methodText.Append ("return ");
489
methodText.Append (param.Text.Trim ());
490
methodText.Append (";");
492
StringBuilder text = new StringBuilder ();
493
if (param.OneChangedVariable) {
494
var par = param.Variables.First (p => p.IsDefinedInsideCutRegion && p.UsedAfterCutRegion);
495
if (!par.UsedInCutRegion) {
497
text.Append (new CSharpAmbience ().GetString (par.ReturnType, OutputFlags.ClassBrowserEntries));
499
text.Append (par.Name);
500
text.AppendLine (";");
503
text.Append (param.Text);
504
if (param.OneChangedVariable) {
506
text.Append ("return ");
507
text.Append (param.Variables.First (p => p.IsDefinedInsideCutRegion && p.UsedAfterCutRegion).Name);
510
methodText.Append (AddIndent (text.ToString (), indent + "\t"));
513
methodText.Append (code.Substring (idx2 + 1));
514
if (param.InsertionPoint != null) {
515
switch (param.InsertionPoint.LineAfter) {
516
case NewLineInsertion.Eol:
517
methodText.AppendLine ();
519
case NewLineInsertion.BlankLine:
520
methodText.AppendLine ();
521
methodText.Append (indent);
522
methodText.AppendLine ();
524
case NewLineInsertion.None:
525
methodText.AppendLine ();
529
methodText.AppendLine ();
530
methodText.Append (indent);
531
methodText.AppendLine ();
533
return methodText.ToString ();
536
public override List<Change> PerformChanges (RefactoringOptions options, object prop)
538
List<Change> result = new List<Change> ();
539
ExtractMethodParameters param = (ExtractMethodParameters)prop;
540
var data = options.GetTextEditorData ();
541
// IResolver resolver = options.GetResolver ();
543
TextReplaceChange replacement = new TextReplaceChange ();
544
replacement.Description = string.Format (GettextCatalog.GetString ("Substitute selected statement(s) with call to {0}"), param.Name);
545
replacement.FileName = options.Document.FileName;
546
replacement.Offset = param.StartOffset;
547
replacement.RemovedChars = param.EndOffset - param.StartOffset;
548
replacement.MoveCaretToReplace = true;
549
replacement.InsertedText = GenerateMethodCall (options, param);
550
result.Add (replacement);
552
TextReplaceChange insertNewMethod = new TextReplaceChange ();
553
insertNewMethod.FileName = options.Document.FileName;
554
insertNewMethod.Description = string.Format (GettextCatalog.GetString ("Create new method {0} from selected statement(s)"), param.Name);
555
var insertionPoint = param.InsertionPoint;
556
if (insertionPoint == null) {
557
var points = CodeGenerationService.GetInsertionPoints (options.Document, param.DeclaringMember.DeclaringType);
558
insertionPoint = points.LastOrDefault (p => p.Location.Line < param.DeclaringMember.Location.Line);
559
if (insertionPoint == null)
560
insertionPoint = points.FirstOrDefault ();
563
insertNewMethod.RemovedChars = 0; //insertionPoint.LineBefore == NewLineInsertion.Eol ? 0 : insertionPoint.Location.Column - 1;
564
insertNewMethod.Offset = data.Document.LocationToOffset (insertionPoint.Location) - insertNewMethod.RemovedChars;
565
insertNewMethod.InsertedText = GenerateMethodDeclaration (options, param);
566
result.Add (insertNewMethod);
569
ExtractMethodAstTransformer transformer = new ExtractMethodAstTransformer (param.VariablesToGenerate);
570
node.AcceptVisitor (transformer, null);
571
if (!param.OneChangedVariable && node is Expression) {
572
ResolveResult resolveResult = resolver.Resolve (new ExpressionResult ("(" + provider.OutputNode (options.Dom, node) + ")"), new DomLocation (options.Document.Editor.Caret.Line, options.Document.Editor.Caret.Column));
573
if (resolveResult.ResolvedType != null)
574
returnType = options.ShortenTypeName (resolveResult.ResolvedType).ConvertToTypeReference ();
577
MethodDeclaration methodDecl = new MethodDeclaration ();
578
methodDecl.Name = param.Name;
579
methodDecl.Modifier = param.Modifiers;
580
methodDecl.TypeReference = returnType;
583
if (node is BlockStatement) {
584
methodDecl.Body = new BlockStatement ();
585
methodDecl.Body.AddChild (new EmptyStatement ());
586
if (param.OneChangedVariable)
587
methodDecl.Body.AddChild (new ReturnStatement (new IdentifierExpression (param.ChangedVariables.First ())));
588
} else if (node is Expression) {
589
methodDecl.Body = new BlockStatement ();
590
methodDecl.Body.AddChild (new ReturnStatement (node as Expression));
593
foreach (VariableDescriptor var in param.VariablesToDefine) {
594
BlockStatement block = methodDecl.Body;
595
LocalVariableDeclaration varDecl = new LocalVariableDeclaration (options.ShortenTypeName (var.ReturnType).ConvertToTypeReference ());
596
varDecl.Variables.Add (new VariableDeclaration (var.Name));
597
block.Children.Insert (0, varDecl);
602
string indent = options.GetIndent (param.DeclaringMember);
603
StringBuilder methodText = new StringBuilder ();
604
switch (param.InsertionPoint.LineBefore) {
605
case NewLineInsertion.Eol:
606
methodText.AppendLine ();
608
case NewLineInsertion.BlankLine:
609
methodText.Append (indent);
610
methodText.AppendLine ();
613
if (param.GenerateComment) {
614
methodText.Append (indent);
615
methodText.AppendLine ("/// <summary>");
616
methodText.Append (indent);
617
methodText.AppendLine ("/// TODO: write a comment.");
618
methodText.Append (indent);
619
methodText.AppendLine ("/// </summary>");
620
Ambience ambience = AmbienceService.GetAmbienceForFile (options.Document.FileName);
621
foreach (ParameterDeclarationExpression pde in methodDecl.Parameters) {
622
methodText.Append (indent);
623
methodText.Append ("/// <param name=\"");
624
methodText.Append (pde.ParameterName);
625
methodText.Append ("\"> A ");
626
methodText.Append (ambience.GetString (pde.TypeReference.ConvertToReturnType (), OutputFlags.IncludeGenerics | OutputFlags.UseFullName));
627
methodText.Append (" </param>");
628
methodText.AppendLine ();
630
if (methodDecl.TypeReference.Type != "System.Void") {
631
methodText.Append (indent);
632
methodText.AppendLine ("/// <returns>");
633
methodText.Append (indent);
634
methodText.Append ("/// A ");
635
methodText.AppendLine (ambience.GetString (methodDecl.TypeReference.ConvertToReturnType (), OutputFlags.IncludeGenerics | OutputFlags.UseFullName));
636
methodText.Append (indent);
637
methodText.AppendLine ("/// </returns>");
641
methodText.Append (indent);
643
if (node is BlockStatement) {
644
string text = provider.OutputNode (options.Dom, methodDecl, indent).Trim ();
645
int emptyStatementMarker = text.LastIndexOf (';');
646
if (param.OneChangedVariable)
647
emptyStatementMarker = text.LastIndexOf (';', emptyStatementMarker - 1);
648
StringBuilder sb = new StringBuilder ();
649
sb.Append (text.Substring (0, emptyStatementMarker));
650
sb.Append (AddIndent (param.Text, indent + "\t"));
651
sb.Append (text.Substring (emptyStatementMarker + 1));
653
methodText.Append (sb.ToString ());
655
methodText.Append (provider.OutputNode (options.Dom, methodDecl, options.GetIndent (param.DeclaringMember)).Trim ());