<commit_before>package org.rstudio.studio.client.workbench.views.source.editors.text;

import com.google.gwt.core.client.JsArrayString;

import org.rstudio.studio.client.common.filetypes.TextFileType;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Position;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Range;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.TokenCursor;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Stack;

public class TextEditingTargetRenameHelper
{
   public TextEditingTargetRenameHelper(DocDisplay docDisplay)
   {
      editor_ = (AceEditor) docDisplay;
      state_ = new Stack<Integer>();
      protectedNamesList_ = new ArrayList<Set<String>>();
      ranges_ = new ArrayList<Range>();
   }
   
   public int renameInScope()
   {
      init();
      TextFileType type = editor_.getFileType();
      if (type.isR() || type.isRhtml() || type.isRmd() || type.isRnw() || type.isRpres())
         return renameInScopeR();
      
      return 0;
   }
   
   private int renameInScopeR()
   {
      Position startPosition = editor_.hasSelection() ?
            editor_.getSelectionStart() :
            editor_.getCursorPosition();
            
      TokenCursor cursor = editor_.getSession().getMode().getCodeModel().getTokenCursor();
      if (!cursor.moveToPosition(startPosition, true))
         return 0;
      
      if (cursor.isRightBracket())
         if (!cursor.moveToPreviousToken())
            return 0;
      
      editor_.setCursorPosition(cursor.currentPosition());
      
      // Ensure the scope tree is built, since we use that for determining
      // the scope for the current refactor.
      editor_.buildScopeTree();
      
      // Validate that we're looking at an R identifier
      String targetValue = cursor.currentValue();
      String targetType = cursor.currentType();
      
      boolean isRefactorable =
            cursor.hasType("identifier", "constant.language", "string") ||
            cursor.typeEquals("keyword");
      
      if (!isRefactorable)
         return 0;
      
      // If we're refactoring a string, just do it through the whole document.
      if (cursor.typeEquals("string"))
      {
         editor_.selectAll(cursor.currentValue());
         return editor_.getNativeSelection().getAllRanges().length;
      }
      
      // Check to see if we're refactoring the name of an argument in a function call,
      // e.g.
      //
      //    if (foo(apple = 1)) { ... }
      //            ^^^^^
      //
      // If this is the case, then we want to search for all calls of this
      // form and rename just that argument.
      if (cursor.peekFwd(1).valueEquals("=") && cursor.isWithinFunctionCall())
      {
         String argName = cursor.currentValue();
         TokenCursor clone = cursor.cloneCursor();
         
         while (clone.findOpeningBracket("(", false))
         {
            if (!clone.moveToPreviousToken())
               break;
            
            String functionName = clone.currentValue();
            return renameFunctionArgument(functionName, argName);
            
         }
      }
      
      // Determine the appropriate refactoring scope.
      //
      // Algorithm:
      //
      //    1. Get the function scope.
      //    2. If we have an argument of the same name, or function
      //       of the same name, rename in that scope.
      //    3. Otherwise, walk forward from the start of that scope,
      //       looking for assignments.
      //    4. If we discover an assignment, rename in that scope
      //       from that position.
      //    5. Repeat while not at top level.
      //
      // TODO: if renaming a function argument, we should also rename
      //       named usages of that function argument where possible.
      Scope scope = editor_.getScopeAtPosition(cursor.currentPosition());
      
      // If the cursor is on the name of a function, then pop up one scope.
      // We want to consider scopes as 'within' the braces, but the 'preamble'
      // of a scope begins with the function identifier.
      if (cursor.peekFwd(1).isLeftAssign() &&
          cursor.peekFwd(2).valueEquals("function") &&
          !scope.isTopLevel())
      {
         scope = scope.getParentScope();
      }
      
      while (!scope.isTopLevel())
      {
         if (scope.isFunction())
         {
            ScopeFunction scopeFn = (ScopeFunction) scope;
            
            String fnName = scopeFn.getFunctionName();
            if (fnName.equals(targetValue))
               return renameVariablesInScope(scope, targetValue, targetType);
            
            JsArrayString fnArgs = scopeFn.getFunctionArgs();
            for (int i = 0; i < fnArgs.length(); i++)
               if (fnArgs.get(i).equals(targetValue))
                  return renameVariablesInScope(scope, targetValue, targetType);
         }
         
         if (!cursor.moveToPosition(scope.getBodyStart(), true))
            continue;
         
         while (cursor.moveToNextToken())
         {
            if (cursor.fwdToMatchingToken())
               continue;
            
            if (cursor.currentPosition().isAfterOrEqualTo(scope.getEnd()) ||
                cursor.currentPosition().isAfterOrEqualTo(startPosition))
            {
               break;
            }
            
            if (cursor.peekFwd(1).isLeftAssign() &&
                !cursor.peekBwd(1).isExtractionOperator())
            {
               return renameVariablesInScope(
                     scope,
                     cursor.currentPosition(),
                     targetValue,
                     targetType);
            }
         }
         
         scope = scope.getParentScope();
      }
      
      return renameVariablesInScope(
            scope,
            targetValue,
            targetType);
   }
   
   private int renameFunctionArgument(String functionName, String argName)
   {
      TokenCursor cursor = editor_.getSession().getMode().getCodeModel().getTokenCursor();
      Stack<String> functionNames = new Stack<String>();
      boolean renaming = false;
      
      do
      {
         if (cursor.isLeftBracket())
         {
            if (cursor.valueEquals("(") &&
                cursor.peekBwd(1).isValidForFunctionCall())
            {
               String currentFunctionName = cursor.peekBwd(1).getValue();
               renaming = currentFunctionName.equals(functionName);
               functionNames.push(functionName);
               pushState(STATE_FUNCTION_CALL);
            }
            else
            {
               pushState(STATE_DEFAULT);
            }
         }
         
         if (cursor.isRightBracket())
         {
            popState();
            if (cursor.valueEquals(")") && !functionNames.empty())
            {
               functionNames.pop();
               renaming = !functionNames.empty() && functionNames.peek().equals(functionName);
            }
         }
         
         if (renaming &&
             peekState() == STATE_FUNCTION_CALL &&
             cursor.valueEquals(argName) &&
             cursor.peekFwd(1).valueEquals("="))
         {
            ranges_.add(getTokenRange(cursor));
         }
         
      } while (cursor.moveToNextToken());
      
      return applyRanges();
      
   }
   
   private int renameVariablesInScope(Scope scope,
                                      String targetValue,
                                      String targetType)
   {
      return renameVariablesInScope(scope, scope.getPreamble(), targetValue, targetType);
   }
   
   private int renameVariablesInScope(Scope scope,
                                      Position startPos,
                                      String targetValue,
                                      String targetType)
   {
      Position endPos = scope.getEnd();
      
      if (endPos == null)
         endPos = Position.create(editor_.getSession().getLength(), 0);
      
      TokenCursor cursor = editor_.getSession().getMode().getCodeModel().getTokenCursor();
      cursor.moveToPosition(startPos, true);
      
      // Workaround 'moveToPosition' not handling forward searches (yet)
      if (cursor.getRow() < startPos.getRow())
         if (!cursor.moveToNextToken())
            return 0;
      
      // NOTE: The token associated with the current cursor position
      // is added last, to ensure that after the 'multi-select' session has
      // ended the cursor remains where it started.
      Position cursorPos = editor_.getCursorPosition();
      
      do
      {
         // Left brackets push on the stack.
         if (cursor.isLeftBracket())
         {
            // Update state.
            if (cursor.valueEquals("("))
            {
               if (cursor.peekBwd(1).valueEquals("function"))
                  pushState(STATE_FUNCTION_DEFINITION);
               else
                  pushState(STATE_FUNCTION_CALL);
            }
            else
            {
               pushState(STATE_DEFAULT);
            }
            
            // Update protected names for braces.
            if (cursor.valueEquals("{"))
               pushProtectedNames(cursor.currentPosition(), scope);
            
            continue;
         }
         
         // Right brackets pop the stack.
         if (cursor.isRightBracket())
         {
            popState();
            if (cursor.valueEquals("}"))
               popProtectedNames(cursor.currentPosition());
            
            continue;
         }
         
         // Protect a name if it's the target of an assignment in a child scope.
         if (cursor.hasType("identifier") &&
             cursor.peekFwd(1).isLeftAssign() &&
             !cursor.peekBwd(1).isExtractionOperator())
         {
            Scope candidate = editor_.getScopeAtPosition(cursor.currentPosition());
            if (cursor.peekFwd(2).valueEquals("function") && !candidate.isTopLevel())
               candidate = candidate.getParentScope();
            
            if (candidate != scope)
            {
               addProtectedName(cursor.currentValue());
               continue;
            }
         }
         
         // Bail if we've reached the end of the scope.
         if (cursor.currentPosition().isAfterOrEqualTo(endPos))
            break;
         
         if (cursor.currentValue().equals(targetValue))
         {
            // Skip 'protected' names. These are names that have been overwritten
            // either as assignments, or exist as names to newly defined functions.
            if (isProtectedName(cursor.currentValue()))
               continue;
            
            // Don't rename the argument names for named function calls.
            // For example, if we're refactoring a variable named 'bar', we
            // only want to refactor the underlined pieces:
            //
            //    bar <- bar + 1; foo(bar = bar)
            //    ~~~    ~~~                ~~~
            if (peekState() == STATE_FUNCTION_CALL && cursor.nextValue().equals("="))
               continue;
            
            // Don't rename argument names in function definitions.
            // For example, if we're refactoring a variable named 'bar', we
            // only want to refactor the underlined pieces:
            //
            //    bar <- bar + 1; foo <- function(bar) { ... }
            //    ~~~    ~~~        
            //<commit_msg>skip variables following e.g. '$'<commit_after>package org.rstudio.studio.client.workbench.views.source.editors.text;

import com.google.gwt.core.client.JsArrayString;

import org.rstudio.studio.client.common.filetypes.TextFileType;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Position;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Range;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.TokenCursor;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Stack;

public class TextEditingTargetRenameHelper
{
   public TextEditingTargetRenameHelper(DocDisplay docDisplay)
   {
      editor_ = (AceEditor) docDisplay;
      state_ = new Stack<Integer>();
      protectedNamesList_ = new ArrayList<Set<String>>();
      ranges_ = new ArrayList<Range>();
   }
   
   public int renameInScope()
   {
      init();
      TextFileType type = editor_.getFileType();
      if (type.isR() || type.isRhtml() || type.isRmd() || type.isRnw() || type.isRpres())
         return renameInScopeR();
      
      return 0;
   }
   
   private int renameInScopeR()
   {
      Position startPosition = editor_.hasSelection() ?
            editor_.getSelectionStart() :
            editor_.getCursorPosition();
            
      TokenCursor cursor = editor_.getSession().getMode().getCodeModel().getTokenCursor();
      if (!cursor.moveToPosition(startPosition, true))
         return 0;
      
      if (cursor.isRightBracket())
         if (!cursor.moveToPreviousToken())
            return 0;
      
      editor_.setCursorPosition(cursor.currentPosition());
      
      // Ensure the scope tree is built, since we use that for determining
      // the scope for the current refactor.
      editor_.buildScopeTree();
      
      // Validate that we're looking at an R identifier
      String targetValue = cursor.currentValue();
      String targetType = cursor.currentType();
      
      boolean isRefactorable =
            cursor.hasType("identifier", "constant.language", "string") ||
            cursor.typeEquals("keyword");
      
      if (!isRefactorable)
         return 0;
      
      // If we're refactoring a string, just do it through the whole document.
      if (cursor.typeEquals("string"))
      {
         editor_.selectAll(cursor.currentValue());
         return editor_.getNativeSelection().getAllRanges().length;
      }
      
      // Check to see if we're refactoring the name of an argument in a function call,
      // e.g.
      //
      //    if (foo(apple = 1)) { ... }
      //            ^^^^^
      //
      // If this is the case, then we want to search for all calls of this
      // form and rename just that argument.
      if (cursor.peekFwd(1).valueEquals("=") && cursor.isWithinFunctionCall())
      {
         String argName = cursor.currentValue();
         TokenCursor clone = cursor.cloneCursor();
         
         while (clone.findOpeningBracket("(", false))
         {
            if (!clone.moveToPreviousToken())
               break;
            
            String functionName = clone.currentValue();
            return renameFunctionArgument(functionName, argName);
            
         }
      }
      
      // Determine the appropriate refactoring scope.
      //
      // Algorithm:
      //
      //    1. Get the function scope.
      //    2. If we have an argument of the same name, or function
      //       of the same name, rename in that scope.
      //    3. Otherwise, walk forward from the start of that scope,
      //       looking for assignments.
      //    4. If we discover an assignment, rename in that scope
      //       from that position.
      //    5. Repeat while not at top level.
      //
      // TODO: if renaming a function argument, we should also rename
      //       named usages of that function argument where possible.
      Scope scope = editor_.getScopeAtPosition(cursor.currentPosition());
      
      // If the cursor is on the name of a function, then pop up one scope.
      // We want to consider scopes as 'within' the braces, but the 'preamble'
      // of a scope begins with the function identifier.
      if (cursor.peekFwd(1).isLeftAssign() &&
          cursor.peekFwd(2).valueEquals("function") &&
          !scope.isTopLevel())
      {
         scope = scope.getParentScope();
      }
      
      while (!scope.isTopLevel())
      {
         if (scope.isFunction())
         {
            ScopeFunction scopeFn = (ScopeFunction) scope;
            
            String fnName = scopeFn.getFunctionName();
            if (fnName.equals(targetValue))
               return renameVariablesInScope(scope, targetValue, targetType);
            
            JsArrayString fnArgs = scopeFn.getFunctionArgs();
            for (int i = 0; i < fnArgs.length(); i++)
               if (fnArgs.get(i).equals(targetValue))
                  return renameVariablesInScope(scope, targetValue, targetType);
         }
         
         if (!cursor.moveToPosition(scope.getBodyStart(), true))
            continue;
         
         while (cursor.moveToNextToken())
         {
            if (cursor.fwdToMatchingToken())
               continue;
            
            if (cursor.currentPosition().isAfterOrEqualTo(scope.getEnd()) ||
                cursor.currentPosition().isAfterOrEqualTo(startPosition))
            {
               break;
            }
            
            if (cursor.peekFwd(1).isLeftAssign() &&
                !cursor.peekBwd(1).isExtractionOperator())
            {
               return renameVariablesInScope(
                     scope,
                     cursor.currentPosition(),
                     targetValue,
                     targetType);
            }
         }
         
         scope = scope.getParentScope();
      }
      
      return renameVariablesInScope(
            scope,
            targetValue,
            targetType);
   }
   
   private int renameFunctionArgument(String functionName, String argName)
   {
      TokenCursor cursor = editor_.getSession().getMode().getCodeModel().getTokenCursor();
      Stack<String> functionNames = new Stack<String>();
      boolean renaming = false;
      
      do
      {
         if (cursor.isLeftBracket())
         {
            if (cursor.valueEquals("(") &&
                cursor.peekBwd(1).isValidForFunctionCall())
            {
               String currentFunctionName = cursor.peekBwd(1).getValue();
               renaming = currentFunctionName.equals(functionName);
               functionNames.push(functionName);
               pushState(STATE_FUNCTION_CALL);
            }
            else
            {
               pushState(STATE_DEFAULT);
            }
         }
         
         if (cursor.isRightBracket())
         {
            popState();
            if (cursor.valueEquals(")") && !functionNames.empty())
            {
               functionNames.pop();
               renaming = !functionNames.empty() && functionNames.peek().equals(functionName);
            }
         }
         
         if (renaming &&
             peekState() == STATE_FUNCTION_CALL &&
             cursor.valueEquals(argName) &&
             cursor.peekFwd(1).valueEquals("="))
         {
            ranges_.add(getTokenRange(cursor));
         }
         
      } while (cursor.moveToNextToken());
      
      return applyRanges();
      
   }
   
   private int renameVariablesInScope(Scope scope,
                                      String targetValue,
                                      String targetType)
   {
      return renameVariablesInScope(scope, scope.getPreamble(), targetValue, targetType);
   }
   
   private int renameVariablesInScope(Scope scope,
                                      Position startPos,
                                      String targetValue,
                                      String targetType)
   {
      Position endPos = scope.getEnd();
      
      if (endPos == null)
         endPos = Position.create(editor_.getSession().getLength(), 0);
      
      TokenCursor cursor = editor_.getSession().getMode().getCodeModel().getTokenCursor();
      cursor.moveToPosition(startPos, true);
      
      // Workaround 'moveToPosition' not handling forward searches (yet)
      if (cursor.getRow() < startPos.getRow())
         if (!cursor.moveToNextToken())
            return 0;
      
      // NOTE: The token associated with the current cursor position
      // is added last, to ensure that after the 'multi-select' session has
      // ended the cursor remains where it started.
      Position cursorPos = editor_.getCursorPosition();
      
      do
      {
         // Left brackets push on the stack.
         if (cursor.isLeftBracket())
         {
            // Update state.
            if (cursor.valueEquals("("))
            {
               if (cursor.peekBwd(1).valueEquals("function"))
                  pushState(STATE_FUNCTION_DEFINITION);
               else
                  pushState(STATE_FUNCTION_CALL);
            }
            else
            {
               pushState(STATE_DEFAULT);
            }
            
            // Update protected names for braces.
            if (cursor.valueEquals("{"))
               pushProtectedNames(cursor.currentPosition(), scope);
            
            continue;
         }
         
         // Right brackets pop the stack.
         if (cursor.isRightBracket())
         {
            popState();
            if (cursor.valueEquals("}"))
               popProtectedNames(cursor.currentPosition());
            
            continue;
         }
         
         // Protect a name if it's the target of an assignment in a child scope.
         if (cursor.hasType("identifier") &&
             cursor.peekFwd(1).isLeftAssign() &&
             !cursor.peekBwd(1).isExtractionOperator())
         {
            Scope candidate = editor_.getScopeAtPosition(cursor.currentPosition());
            if (cursor.peekFwd(2).valueEquals("function") && !candidate.isTopLevel())
               candidate = candidate.getParentScope();
            
            if (candidate != scope)
            {
               addProtectedName(cursor.currentValue());
               continue;
            }
         }
         
         // Bail if we've reached the end of the scope.
         if (cursor.currentPosition().isAfterOrEqualTo(endPos))
            break;
         
         if (cursor.currentValue().equals(targetValue))
         {
            // Skip 'protected' names. These are names that have been overwritten
            // either as assignments, or exist as names to newly defined functions.
            if (isProtectedName(cursor.currentValue()))
               continue;
            
            // Skip variables following an 'extraction' operator.
            if (cursor.peekBwd(1).isExtractionOperator())
               continue;
            
            // Don't rename the argument names for named function calls.
            // For example, if we're refactoring a variable named 'bar', we
            // only want to refactor the underlined pieces:
            //
            //    bar <- bar + 1; foo(bar = bar)
            //    ~~~    ~~~                ~~~
            if (peekState() == STATE_FUNCTION_CALL && cursor.nextValue().equals("="))
               continue;
            
            // Don't rename argument names in function definitions.
            // For example, if we're refactoring a variable named 'bar', we
            // only want to refactor the underlined pieces:
            //
            //    bar <- bar + 1; foo <- function(bar) { ... }
            //    ~~~    ~~~        
            //
{'fraction_non_alphanumeric': 0.10209580838323354, 'fraction_numerical': 0.002352437981180496, 'mean_word_length': 1.6270786516853932, 'pattern_counts': {'":': 0, '<': 25, '<?xml version=': 0, '>': 15, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 10, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5017992', 'n_tokens_mistral': 6007, 'n_tokens_neox': 5853, 'n_words': 1409}