@Nullable("null means we were unable to calculate")
  LogicalPosition hostToInjectedInVirtualSpace(@NotNull LogicalPosition hPos) {
    // beware the virtual space
    int hLineStartOffset =
        hPos.line >= myDelegate.getLineCount()
            ? myDelegate.getTextLength()
            : myDelegate.getLineStartOffset(hPos.line);
    int iLineStartOffset = hostToInjected(hLineStartOffset);
    int iLine = getLineNumber(iLineStartOffset);

    synchronized (myLock) {
      for (int i = myShreds.size() - 1; i >= 0; i--) {
        PsiLanguageInjectionHost.Shred shred = myShreds.get(i);
        if (!shred.isValid()) continue;
        Segment hostRangeMarker = shred.getHostRangeMarker();
        if (hostRangeMarker == null) continue;
        int hShredEndOffset = hostRangeMarker.getEndOffset();
        int hShredStartOffset = hostRangeMarker.getStartOffset();

        int hShredStartLine = myDelegate.getLineNumber(hShredStartOffset);
        int hShredEndLine = myDelegate.getLineNumber(hShredEndOffset);

        if (hShredStartLine <= hPos.line && hPos.line <= hShredEndLine) {
          int hColumnOfShredEnd = hShredEndOffset - hLineStartOffset;
          int iColumnOfShredEnd = hostToInjected(hShredEndOffset) - iLineStartOffset;
          int iColumn = iColumnOfShredEnd + hPos.column - hColumnOfShredEnd;
          return new LogicalPosition(iLine, iColumn);
        }
      }
    }

    return null;
  }
 public DocumentWindowImpl(@NotNull DocumentEx delegate, boolean oneLine, @NotNull Place shreds) {
   myDelegate = delegate;
   myOneLine = oneLine;
   synchronized (myLock) {
     myShreds = shreds;
   }
   myPrefixLineCount = Math.max(1, 1 + StringUtil.countNewLines(shreds.get(0).getPrefix()));
   mySuffixLineCount =
       Math.max(1, 1 + StringUtil.countNewLines(shreds.get(shreds.size() - 1).getSuffix()));
 }
 @Override
 public boolean isValid() {
   PsiLanguageInjectionHost.Shred[] shreds;
   synchronized (myLock) {
     shreds = myShreds.toArray(new PsiLanguageInjectionHost.Shred[myShreds.size()]);
   }
   // can grab PsiLock in SmartPsiPointer.restore()
   for (PsiLanguageInjectionHost.Shred shred : shreds) {
     if (!shred.isValid()) return false;
   }
   return true;
 }
 private int injectedToHost(int offset, boolean preferLeftFragment) {
   synchronized (myLock) {
     if (offset < myShreds.get(0).getPrefix().length()) {
       Segment hostRangeMarker = myShreds.get(0).getHostRangeMarker();
       return hostRangeMarker == null ? 0 : hostRangeMarker.getStartOffset();
     }
     int prevEnd = 0;
     for (int i = 0; i < myShreds.size(); i++) {
       Segment currentRange = myShreds.get(i).getHostRangeMarker();
       if (currentRange == null) continue;
       offset -= myShreds.get(i).getPrefix().length();
       int length = currentRange.getEndOffset() - currentRange.getStartOffset();
       if (offset < 0) {
         return preferLeftFragment ? prevEnd : currentRange.getStartOffset() - 1;
       }
       if (offset == 0) {
         return preferLeftFragment && i != 0 ? prevEnd : currentRange.getStartOffset();
       }
       if (offset < length || offset == length && preferLeftFragment) {
         return currentRange.getStartOffset() + offset;
       }
       offset -= length;
       offset -= myShreds.get(i).getSuffix().length();
       prevEnd = currentRange.getEndOffset();
     }
     Segment hostRangeMarker = myShreds.get(myShreds.size() - 1).getHostRangeMarker();
     return hostRangeMarker == null ? 0 : hostRangeMarker.getEndOffset();
   }
 }
  private void doReplaceString(int startOffset, int endOffset, CharSequence s) {
    assert intersectWithEditable(new TextRange(startOffset, startOffset)) != null;
    assert intersectWithEditable(new TextRange(endOffset, endOffset)) != null;

    List<Pair<TextRange, CharSequence>> hostRangesToModify;
    synchronized (myLock) {
      hostRangesToModify = new ArrayList<Pair<TextRange, CharSequence>>(myShreds.size());

      int offset = startOffset;
      int curRangeStart = 0;
      for (int i = 0; i < myShreds.size(); i++) {
        PsiLanguageInjectionHost.Shred shred = myShreds.get(i);
        curRangeStart += shred.getPrefix().length();
        if (offset < curRangeStart) offset = curRangeStart;
        Segment hostRange = shred.getHostRangeMarker();
        if (hostRange == null) continue;
        int hostRangeLength = hostRange.getEndOffset() - hostRange.getStartOffset();
        TextRange range = TextRange.from(curRangeStart, hostRangeLength);
        if (range.contains(offset)
            || range.getEndOffset() == offset /* in case of inserting at the end*/) {
          TextRange rangeToModify =
              new TextRange(offset, Math.min(range.getEndOffset(), endOffset));
          TextRange hostRangeToModify =
              rangeToModify.shiftRight(hostRange.getStartOffset() - curRangeStart);
          CharSequence toReplace =
              i == myShreds.size() - 1
                      || range.getEndOffset() + shred.getSuffix().length() >= endOffset
                  ? s
                  : s.subSequence(0, Math.min(hostRangeToModify.getLength(), s.length()));
          s = toReplace == s ? "" : s.subSequence(toReplace.length(), s.length());
          hostRangesToModify.add(Pair.create(hostRangeToModify, toReplace));
          offset = rangeToModify.getEndOffset();
        }
        curRangeStart += hostRangeLength;
        curRangeStart += shred.getSuffix().length();
        if (curRangeStart > endOffset) break;
      }
    }

    int delta = 0;
    for (Pair<TextRange, CharSequence> pair : hostRangesToModify) {
      TextRange hostRange = pair.getFirst();
      CharSequence replace = pair.getSecond();

      myDelegate.replaceString(
          hostRange.getStartOffset() + delta, hostRange.getEndOffset() + delta, replace);
      delta -= hostRange.getLength() - replace.length();
    }
  }
 private void initGuardedBlocks(Place shreds) {
   int origOffset = -1;
   int curOffset = 0;
   for (PsiLanguageInjectionHost.Shred shred : shreds) {
     Segment hostRangeMarker = shred.getHostRangeMarker();
     int start = shred.getRange().getStartOffset() + shred.getPrefix().length();
     int end = shred.getRange().getEndOffset() - shred.getSuffix().length();
     if (curOffset < start) {
       RangeMarker guard = myNewDocument.createGuardedBlock(curOffset, start);
       if (curOffset == 0 && shred == shreds.get(0)) guard.setGreedyToLeft(true);
       String padding =
           origOffset < 0
               ? ""
               : myOrigDocument.getText().substring(origOffset, hostRangeMarker.getStartOffset());
       guard.putUserData(REPLACEMENT_KEY, fixQuotes(padding));
     }
     curOffset = end;
     origOffset = hostRangeMarker.getEndOffset();
   }
   if (curOffset < myNewDocument.getTextLength()) {
     RangeMarker guard =
         myNewDocument.createGuardedBlock(curOffset, myNewDocument.getTextLength());
     guard.setGreedyToRight(true);
     guard.putUserData(REPLACEMENT_KEY, "");
   }
 }
 // minimum sequence of text replacement operations for each host range
 // result[i] == null means no change
 // result[i] == "" means delete
 // result[i] == string means replace
 public String[] calculateMinEditSequence(String newText) {
   synchronized (myLock) {
     String[] result = new String[myShreds.size()];
     String hostText = myDelegate.getText();
     calculateMinEditSequence(hostText, newText, result, 0, result.length - 1);
     for (int i = 0; i < result.length; i++) {
       String change = result[i];
       if (change == null) continue;
       String prefix = myShreds.get(i).getPrefix();
       String suffix = myShreds.get(i).getSuffix();
       assert change.startsWith(prefix) : change + "/" + prefix;
       assert change.endsWith(suffix) : change + "/" + suffix;
       result[i] = StringUtil.trimEnd(StringUtil.trimStart(change, prefix), suffix);
     }
     return result;
   }
 }
 @Override
 public void setText(@NotNull CharSequence text) {
   synchronized (myLock) {
     LOG.assertTrue(text.toString().startsWith(myShreds.get(0).getPrefix()));
     LOG.assertTrue(text.toString().endsWith(myShreds.get(myShreds.size() - 1).getSuffix()));
     if (isOneLine()) {
       text = StringUtil.replace(text.toString(), "\n", "");
     }
     String[] changes = calculateMinEditSequence(text.toString());
     assert changes.length == myShreds.size();
     for (int i = 0; i < changes.length; i++) {
       String change = changes[i];
       if (change != null) {
         Segment hostRange = myShreds.get(i).getHostRangeMarker();
         if (hostRange == null) continue;
         myDelegate.replaceString(hostRange.getStartOffset(), hostRange.getEndOffset(), change);
       }
     }
   }
 }
 private String getRangeText(@NotNull String hostText, int hostNum) {
   synchronized (myLock) {
     PsiLanguageInjectionHost.Shred shred = myShreds.get(hostNum);
     Segment hostRangeMarker = shred.getHostRangeMarker();
     return shred.getPrefix()
         + (hostRangeMarker == null
             ? ""
             : hostText.substring(
                 hostRangeMarker.getStartOffset(), hostRangeMarker.getEndOffset()))
         + shred.getSuffix();
   }
 }
 @Override
 public int hostToInjected(int hostOffset) {
   synchronized (myLock) {
     Segment hostRangeMarker = myShreds.get(0).getHostRangeMarker();
     if (hostRangeMarker == null || hostOffset < hostRangeMarker.getStartOffset())
       return myShreds.get(0).getPrefix().length();
     int offset = 0;
     for (int i = 0; i < myShreds.size(); i++) {
       offset += myShreds.get(i).getPrefix().length();
       Segment currentRange = myShreds.get(i).getHostRangeMarker();
       if (currentRange == null) continue;
       Segment nextRange =
           i == myShreds.size() - 1 ? null : myShreds.get(i + 1).getHostRangeMarker();
       if (nextRange == null || hostOffset < nextRange.getStartOffset()) {
         if (hostOffset >= currentRange.getEndOffset()) hostOffset = currentRange.getEndOffset();
         return offset + hostOffset - currentRange.getStartOffset();
       }
       offset += currentRange.getEndOffset() - currentRange.getStartOffset();
       offset += myShreds.get(i).getSuffix().length();
     }
     return getTextLength() - myShreds.get(myShreds.size() - 1).getSuffix().length();
   }
 }
 @Override
 @NotNull
 public Segment[] getHostRanges() {
   synchronized (myLock) {
     List<Segment> markers = new ArrayList<Segment>(myShreds.size());
     for (PsiLanguageInjectionHost.Shred shred : myShreds) {
       Segment hostMarker = shred.getHostRangeMarker();
       if (hostMarker != null) {
         markers.add(hostMarker);
       }
     }
     return markers.isEmpty() ? Segment.EMPTY_ARRAY : markers.toArray(new Segment[markers.size()]);
   }
 }
 @Override
 public void insertString(final int offset, @NotNull CharSequence s) {
   synchronized (myLock) {
     LOG.assertTrue(offset >= myShreds.get(0).getPrefix().length(), myShreds.get(0).getPrefix());
     LOG.assertTrue(
         offset <= getTextLength() - myShreds.get(myShreds.size() - 1).getSuffix().length(),
         myShreds.get(myShreds.size() - 1).getSuffix());
   }
   if (isOneLine()) {
     s = StringUtil.replace(s.toString(), "\n", "");
   }
   myDelegate.insertString(injectedToHost(offset), s);
 }
  @Override
  public int injectedToHostLine(int line) {
    if (line < myPrefixLineCount) {
      synchronized (myLock) {
        Segment hostRangeMarker = myShreds.get(0).getHostRangeMarker();
        return hostRangeMarker == null
            ? 0
            : myDelegate.getLineNumber(hostRangeMarker.getStartOffset());
      }
    }
    int lineCount = getLineCount();
    if (line > lineCount - mySuffixLineCount) {
      return lineCount;
    }
    int offset = getLineStartOffset(line);
    int hostOffset = injectedToHost(offset);

    return myDelegate.getLineNumber(hostOffset);
  }
  @Override
  public void deleteString(final int startOffset, final int endOffset) {
    assert intersectWithEditable(new TextRange(startOffset, startOffset)) != null;
    assert intersectWithEditable(new TextRange(endOffset, endOffset)) != null;

    List<TextRange> hostRangesToDelete;
    synchronized (myLock) {
      hostRangesToDelete = new ArrayList<TextRange>(myShreds.size());

      int offset = startOffset;
      int curRangeStart = 0;
      for (PsiLanguageInjectionHost.Shred shred : myShreds) {
        curRangeStart += shred.getPrefix().length();
        if (offset < curRangeStart) offset = curRangeStart;
        if (offset >= endOffset) break;
        Segment hostRange = shred.getHostRangeMarker();
        if (hostRange == null) continue;
        int hostRangeLength = hostRange.getEndOffset() - hostRange.getStartOffset();
        TextRange range = TextRange.from(curRangeStart, hostRangeLength);
        if (range.contains(offset)) {
          TextRange rangeToDelete =
              new TextRange(offset, Math.min(range.getEndOffset(), endOffset));
          hostRangesToDelete.add(
              rangeToDelete.shiftRight(hostRange.getStartOffset() - curRangeStart));
          offset = rangeToDelete.getEndOffset();
        }
        curRangeStart += hostRangeLength;
        curRangeStart += shred.getSuffix().length();
      }
    }

    int delta = 0;
    for (TextRange hostRangeToDelete : hostRangesToDelete) {
      myDelegate.deleteString(
          hostRangeToDelete.getStartOffset() + delta, hostRangeToDelete.getEndOffset() + delta);
      delta -= hostRangeToDelete.getLength();
    }
  }
  @Override
  public boolean areRangesEqual(@NotNull DocumentWindow otherd) {
    DocumentWindowImpl window = (DocumentWindowImpl) otherd;
    Place shreds = getShreds();
    Place otherShreds = window.getShreds();
    if (shreds.size() != otherShreds.size()) return false;
    for (int i = 0; i < shreds.size(); i++) {
      PsiLanguageInjectionHost.Shred shred = shreds.get(i);
      PsiLanguageInjectionHost.Shred otherShred = otherShreds.get(i);
      if (!shred.getPrefix().equals(otherShred.getPrefix())) return false;
      if (!shred.getSuffix().equals(otherShred.getSuffix())) return false;

      Segment hostRange = shred.getHostRangeMarker();
      Segment other = otherShred.getHostRangeMarker();
      if (hostRange == null
          || other == null
          || hostRange.getStartOffset() != other.getStartOffset()) return false;
      if (hostRange.getEndOffset() != other.getEndOffset()) return false;
    }
    return true;
  }
 private void calculateMinEditSequence(
     String hostText, String newText, String[] result, int i, int j) {
   synchronized (myLock) {
     String rangeText1 = getRangeText(hostText, i);
     if (i == j) {
       result[i] = rangeText1.equals(newText) ? null : newText;
       return;
     }
     if (StringUtil.startsWith(newText, rangeText1)) {
       result[i] = null; // no change
       calculateMinEditSequence(
           hostText, newText.substring(rangeText1.length()), result, i + 1, j);
       return;
     }
     String rangeText2 = getRangeText(hostText, j);
     if (StringUtil.endsWith(newText, rangeText2)) {
       result[j] = null; // no change
       calculateMinEditSequence(
           hostText,
           newText.substring(0, newText.length() - rangeText2.length()),
           result,
           i,
           j - 1);
       return;
     }
     if (i + 1 == j) {
       String suffix = myShreds.get(i).getSuffix();
       String prefix = myShreds.get(j).getPrefix();
       String separator = suffix + prefix;
       if (!separator.isEmpty()) {
         int sep = newText.indexOf(separator);
         assert sep != -1;
         result[i] = newText.substring(0, sep + suffix.length());
         result[j] = newText.substring(sep + suffix.length() + prefix.length(), newText.length());
         return;
       }
       String commonPrefix = StringUtil.commonPrefix(rangeText1, newText);
       result[i] = commonPrefix;
       result[j] = newText.substring(commonPrefix.length());
       return;
     }
     String middleText = getRangeText(hostText, i + 1);
     int m = newText.indexOf(middleText);
     if (m != -1) {
       result[i] = newText.substring(0, m);
       result[i + 1] = null;
       calculateMinEditSequence(
           hostText,
           newText.substring(m + middleText.length(), newText.length()),
           result,
           i + 2,
           j);
       return;
     }
     middleText = getRangeText(hostText, j - 1);
     m = newText.lastIndexOf(middleText);
     if (m != -1) {
       result[j] = newText.substring(m + middleText.length());
       result[j - 1] = null;
       calculateMinEditSequence(hostText, newText.substring(0, m), result, i, j - 2);
       return;
     }
     result[i] = "";
     result[j] = "";
     calculateMinEditSequence(hostText, newText, result, i + 1, j - 1);
   }
 }
 public int hostToInjectedUnescaped(int hostOffset) {
   synchronized (myLock) {
     Segment hostRangeMarker = myShreds.get(0).getHostRangeMarker();
     if (hostRangeMarker == null || hostOffset < hostRangeMarker.getStartOffset())
       return myShreds.get(0).getPrefix().length();
     int offset = 0;
     for (int i = 0; i < myShreds.size(); i++) {
       offset += myShreds.get(i).getPrefix().length();
       Segment currentRange = myShreds.get(i).getHostRangeMarker();
       if (currentRange == null) continue;
       Segment nextRange =
           i == myShreds.size() - 1 ? null : myShreds.get(i + 1).getHostRangeMarker();
       if (nextRange == null || hostOffset < nextRange.getStartOffset()) {
         if (hostOffset >= currentRange.getEndOffset()) {
           offset += myShreds.get(i).getRange().getLength();
         } else {
           // todo use escaper to convert host-range delta into injected space
           offset += hostOffset - currentRange.getStartOffset();
         }
         return offset;
       }
       offset += myShreds.get(i).getRange().getLength();
       offset += myShreds.get(i).getSuffix().length();
     }
     return getTextLength() - myShreds.get(myShreds.size() - 1).getSuffix().length();
   }
 }
 public void setShreds(@NotNull Place shreds) {
   synchronized (myLock) {
     myShreds.dispose();
     myShreds = shreds;
   }
 }
 public int hashCode() {
   synchronized (myLock) {
     Segment hostRangeMarker = myShreds.get(0).getHostRangeMarker();
     return hostRangeMarker == null ? -1 : hostRangeMarker.getStartOffset();
   }
 }
  QuickEditHandler(
      Project project,
      @NotNull PsiFile injectedFile,
      final PsiFile origFile,
      Editor editor,
      QuickEditAction action) {
    myProject = project;
    myEditor = editor;
    myAction = action;
    myOrigDocument = editor.getDocument();
    Place shreds = InjectedLanguageUtil.getShreds(injectedFile);
    FileType fileType = injectedFile.getFileType();
    Language language = injectedFile.getLanguage();
    PsiLanguageInjectionHost.Shred firstShred = ContainerUtil.getFirstItem(shreds);

    PsiFileFactory factory = PsiFileFactory.getInstance(project);
    String text = InjectedLanguageManager.getInstance(project).getUnescapedText(injectedFile);
    String newFileName =
        StringUtil.notNullize(language.getDisplayName(), "Injected")
            + " Fragment "
            + "("
            + origFile.getName()
            + ":"
            + firstShred.getHost().getTextRange().getStartOffset()
            + ")"
            + "."
            + fileType.getDefaultExtension();

    // preserve \r\n as it is done in MultiHostRegistrarImpl
    myNewFile = factory.createFileFromText(newFileName, language, text, true, false);
    myNewVirtualFile = ObjectUtils.assertNotNull((LightVirtualFile) myNewFile.getVirtualFile());
    myNewVirtualFile.setOriginalFile(origFile.getVirtualFile());

    assert myNewFile != null : "PSI file is null";
    assert myNewFile.getTextLength() == myNewVirtualFile.getContent().length()
        : "PSI / Virtual file text mismatch";

    myNewVirtualFile.setOriginalFile(origFile.getVirtualFile());
    // suppress possible errors as in injected mode
    myNewFile.putUserData(
        InjectedLanguageUtil.FRANKENSTEIN_INJECTION,
        injectedFile.getUserData(InjectedLanguageUtil.FRANKENSTEIN_INJECTION));
    myNewFile.putUserData(FileContextUtil.INJECTED_IN_ELEMENT, shreds.getHostPointer());
    myNewDocument = PsiDocumentManager.getInstance(project).getDocument(myNewFile);
    assert myNewDocument != null;
    EditorActionManager.getInstance()
        .setReadonlyFragmentModificationHandler(myNewDocument, new MyQuietHandler());
    myOrigCreationStamp =
        myOrigDocument.getModificationStamp(); // store creation stamp for UNDO tracking
    myOrigDocument.addDocumentListener(this, this);
    myNewDocument.addDocumentListener(this, this);
    EditorFactory editorFactory = ObjectUtils.assertNotNull(EditorFactory.getInstance());
    // not FileEditorManager listener because of RegExp checker and alike
    editorFactory.addEditorFactoryListener(
        new EditorFactoryAdapter() {
          int useCount;

          @Override
          public void editorCreated(@NotNull EditorFactoryEvent event) {
            if (event.getEditor().getDocument() != myNewDocument) return;
            useCount++;
          }

          @Override
          public void editorReleased(@NotNull EditorFactoryEvent event) {
            if (event.getEditor().getDocument() != myNewDocument) return;
            if (--useCount > 0) return;
            if (Boolean.TRUE.equals(
                myNewVirtualFile.getUserData(FileEditorManagerImpl.CLOSING_TO_REOPEN))) return;

            Disposer.dispose(QuickEditHandler.this);
          }
        },
        this);

    if ("JAVA".equals(firstShred.getHost().getLanguage().getID())) {
      PsiLanguageInjectionHost.Shred lastShred = ContainerUtil.getLastItem(shreds);
      myAltFullRange =
          myOrigDocument.createRangeMarker(
              firstShred.getHostRangeMarker().getStartOffset(),
              lastShred.getHostRangeMarker().getEndOffset());
      myAltFullRange.setGreedyToLeft(true);
      myAltFullRange.setGreedyToRight(true);

      initGuardedBlocks(shreds);
      myInjectedFile = null;
    } else {
      initMarkers(shreds);
      myAltFullRange = null;
      myInjectedFile = injectedFile;
    }
  }
 @Override
 public void dispose() {
   synchronized (myLock) {
     myShreds.dispose();
   }
 }