@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(); } }