BooleanRunnable (@NotNull Language language, @NotNull DocumentWindowImpl oldDocumentWindow, @NotNull PsiFile oldInjectedPsi, @NotNull VirtualFileWindow oldInjectedVirtualFile, @NotNull VirtualFile hostVirtualFile, @NotNull PsiFile hostPsiFile, @NotNull DocumentEx hostDocument, @NotNull ProgressIndicator indicator, @NotNull ASTNode oldRoot, @NotNull ASTNode newRoot, @NotNull PsiDocumentManagerBase documentManager) { Project project = hostPsiFile.getProject(); String newText = oldDocumentWindow.getText(); FileASTNode oldNode = oldInjectedPsi.getNode(); InjectedFileViewProvider oldInjectedPsiViewProvider = (InjectedFileViewProvider)oldInjectedPsi.getViewProvider(); String oldPsiText = oldNode.getText(); if (newText.equals(oldPsiText)) return ()->true; if (oldDocumentWindow.isOneLine() && newText.contains("\n") != oldPsiText.contains("\n")) { // one-lineness changed, e.g. when enter pressed in the middle of a string literal return null; } Place oldPlace = oldDocumentWindow.getShreds(); // can be different from newText if decode fails in the middle and we'll have to shrink the document StringBuilder newDocumentText = new StringBuilder(newText.length()); // we need escaper, but it only works with committed PSI, // so we get the committed (but not yet applied) PSI from the commit-document-in-the-background process // and find the corresponding injection host there // and create literal escaper from that new (dummy) psi List<PlaceInfo> placeInfos = new SmartList<>(); StringBuilder chars = new StringBuilder(); for (PsiLanguageInjectionHost.Shred shred : oldPlace) { PsiLanguageInjectionHost oldHost = shred.getHost(); if (oldHost == null) { return null; } SmartPsiElementPointer<PsiLanguageInjectionHost> hostPointer = ((ShredImpl)shred).getSmartPointer(); Segment hostPsiRange = hostPointer.getPsiRange(); Segment newInjectionHostRange = SelfElementInfo.calcActualRangeAfterDocumentEvents(hostPsiFile, hostDocument, hostPsiRange, true); if (newInjectionHostRange == null) { return null; } PsiLanguageInjectionHost newDummyInjectionHost = findNewInjectionHost(hostPsiFile, oldRoot, newRoot, oldHost, newInjectionHostRange); if (newDummyInjectionHost == null) { return null; } newInjectionHostRange = newDummyInjectionHost.getTextRange().shiftRight(oldRoot.getTextRange().getStartOffset()); Segment hostInjectionRange = shred.getHostRangeMarker(); // in the new document if (hostInjectionRange == null) { return null; } TextRange rangeInsideHost = TextRange.create(hostInjectionRange).shiftLeft(newInjectionHostRange.getStartOffset()); PlaceInfo info = new PlaceInfo(shred.getPrefix(), shred.getSuffix(), newDummyInjectionHost, rangeInsideHost); placeInfos.add(info); info.newInjectionHostRange = newInjectionHostRange; decode(info, chars); // pass the old pointers because their offsets will be adjusted automatically (SmartPsiElementPointer does that) TextRange rangeInHostElementPSI = info.rangeInHostElement; newDocumentText.append(shred.getPrefix()); newDocumentText.append(newDummyInjectionHost.getText(), rangeInHostElementPSI.getStartOffset(), rangeInHostElementPSI.getEndOffset()); newDocumentText.append(shred.getSuffix()); } // newDocumentText can be shorter if decode failed //assert newText.equals(newDocumentText.toString()) : "-\n"+newText+"\n--\n"+newDocumentText+"\n---\n"; assert documentManager.isUncommited(hostDocument); String fileName = ((VirtualFileWindowImpl)oldInjectedVirtualFile).getName(); ASTNode[] parsedNodes = parseFile(language, language, oldDocumentWindow, hostVirtualFile, hostDocument, hostPsiFile, project, newDocumentText, placeInfos, chars, fileName, documentManager); List<PsiFile> oldFiles = ((AbstractFileViewProvider)oldInjectedPsiViewProvider).getCachedPsiFiles(); synchronized (InjectedLanguageManagerImpl.ourInjectionPsiLock) { DiffLog[] diffLogs = new DiffLog[parsedNodes.length]; for (int i = 0; i < parsedNodes.length; i++) { ASTNode parsedNode = parsedNodes[i]; PsiFile oldFile = oldFiles.get(i); diffLogs[i] = BlockSupportImpl.mergeTrees((PsiFileImpl)oldFile, oldFile.getNode(), parsedNode, indicator, oldPsiText); } return () -> { oldInjectedPsiViewProvider.performNonPhysically(() -> DebugUtil.performPsiModification("injected tree diff", () -> { for (int i = 0; i < diffLogs.length; i++) { DiffLog diffLog = diffLogs[i]; diffLog.doActualPsiChange(oldFiles.get(i)); // create new shreds after commit is complete because otherwise the range markers will be changed in MarkerCache.updateMarkers Place newPlace = new Place(); for (int j = 0; j < oldPlace.size(); j++) { PsiLanguageInjectionHost.Shred shred = oldPlace.get(j); PlaceInfo info = placeInfos.get(j); TextRange rangeInDecodedPSI = info.rangeInDecodedPSI; TextRange rangeInHostElementPSI = info.rangeInHostElement; // now find the injection host in the newly committed file FileASTNode root = hostPsiFile.getNode(); PsiLanguageInjectionHost newHost = findNewInjectionHost(hostPsiFile, root, root, info.host, info.newInjectionHostRange); ShredImpl newShred = ((ShredImpl)shred).withRange(rangeInDecodedPSI, rangeInHostElementPSI, newHost); newPlace.add(newShred); } cacheEverything(newPlace, oldDocumentWindow, oldInjectedPsiViewProvider, oldFiles.get(i)); String docText = oldDocumentWindow.getText(); assert docText.equals(newText) : "=\n" + docText + "\n==\n" + newDocumentText + "\n===\n"; } }) ); return true; }; } }