[html] IJPL-116992 Use new API for HtmlFileDropHandler

GitOrigin-RevId: 71486cd887fa43a67956925b13c93ec6b311f18d
This commit is contained in:
Yuriy Artamonov
2024-05-11 19:40:24 +02:00
committed by intellij-monorepo-bot
parent 727b5b0e45
commit 63d8a44c12
2 changed files with 218 additions and 212 deletions

View File

@@ -29,7 +29,7 @@
<externalAnnotator language="XHTML"
implementationClass="com.intellij.htmltools.lang.annotation.HtmlNonExistentInternetResourcesAnnotator"/>
<customFileDropHandler implementation="com.intellij.htmltools.ide.HtmlFileDropHandler"/>
<fileDropHandler implementation="com.intellij.htmltools.ide.HtmlFileDropHandler"/>
<localInspection language="XML" shortName="HtmlPresentationalElement"
bundle="messages.HtmlToolsBundle" key="html.inspections.check.presentational.tag"

View File

@@ -1,220 +1,226 @@
/*
* Copyright 2000-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.htmltools.ide;
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.htmltools.ide
import com.intellij.codeInsight.editorActions.XmlEditUtil;
import com.intellij.codeInsight.hint.HintManager;
import com.intellij.codeInsight.hint.HintManagerImpl;
import com.intellij.codeInsight.hint.HintUtil;
import com.intellij.htmltools.HtmlToolsBundle;
import com.intellij.htmltools.xml.util.HtmlReferenceProvider;
import com.intellij.ide.dnd.FileCopyPasteUtil;
import com.intellij.lang.ASTNode;
import com.intellij.lang.Language;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.editor.*;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.fileTypes.LanguageFileType;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.XmlElementFactory;
import com.intellij.psi.impl.source.html.HtmlFileImpl;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.*;
import com.intellij.ui.LightweightHint;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.xml.util.HtmlUtil;
import org.intellij.images.fileTypes.ImageFileTypeManager;
import org.intellij.images.util.ImageInfo;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import com.intellij.codeInsight.editorActions.XmlEditUtil
import com.intellij.codeInsight.hint.HintManager
import com.intellij.codeInsight.hint.HintManagerImpl
import com.intellij.codeInsight.hint.HintUtil
import com.intellij.htmltools.HtmlToolsBundle
import com.intellij.lang.Language
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.readAction
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.editor.*
import com.intellij.openapi.fileTypes.FileType
import com.intellij.openapi.fileTypes.LanguageFileType
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.XmlElementFactory
import com.intellij.psi.impl.source.html.HtmlFileImpl
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.xml.XmlChildRole
import com.intellij.psi.xml.XmlDocument
import com.intellij.psi.xml.XmlElement
import com.intellij.psi.xml.XmlTag
import com.intellij.ui.LightweightHint
import com.intellij.util.concurrency.annotations.RequiresReadLock
import com.intellij.xml.util.HtmlUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.intellij.images.fileTypes.ImageFileTypeManager
import org.intellij.images.index.ImageInfoIndex
import org.intellij.images.util.ImageInfo
import java.io.File
import java.text.MessageFormat
import java.awt.*;
import java.awt.datatransfer.Transferable;
import java.io.File;
import java.text.MessageFormat;
import java.util.List;
internal class HtmlFileDropHandler : FileDropHandler {
override suspend fun handleDrop(e: FileDropEvent): Boolean {
val editor = e.editor ?: return false
/**
* @author Dennis.Ushakov
*/
public final class HtmlFileDropHandler extends CustomFileDropHandler {
@Override
public boolean canHandle(@NotNull Transferable t, @Nullable Editor editor) {
if (editor == null) return false;
final VirtualFile file = getDroppedFile(t);
if (file == null) return false;
final FileType type = file.getFileType();
return isImage(type) || isCSS(type) || isJS(type);
}
val target = readAction {
val target = PsiDocumentManager.getInstance(e.project).getPsiFile(editor.document)
target as? HtmlFileImpl
} ?: return false
@Override
public boolean handleDrop(@NotNull Transferable t, @Nullable Editor editor, Project project) {
assert editor != null;
final Document document = editor.getDocument();
final PsiFile target = PsiDocumentManager.getInstance(project).getPsiFile(document);
if (!(target instanceof HtmlFileImpl)) return false;
val file = getDroppedFile(e.files) ?: return false
final VirtualFile file = getDroppedFile(t);
if (file == null) return false;
final FileType type = file.getFileType();
if (isCSS(type)) {
return insertTagInHead((HtmlFileImpl)target, file, editor, "<link rel={0}stylesheet{0} href={0}{1}{0}>");
}
if (isJS(type)) {
return insertTagInHead((HtmlFileImpl)target, file, editor, "<script src={0}{1}{0}></script>");
}
if (isImage(type)) {
return insertImageTag((HtmlFileImpl)target, file, editor, "<img src={0}{1}{0}/>");
}
return false;
}
private static boolean insertTagInHead(HtmlFileImpl target, VirtualFile droppedFile, Editor editor, String tagTemplate) {
final String relativePath = getRelativePath(target, droppedFile);
final String tagText = prepareTagText(target, tagTemplate, relativePath);
final XmlDocument document = target.getDocument();
if (document == null || tagText == null) return false;
final XmlTag[] topLevelTags = PsiTreeUtil.getChildrenOfType(document, XmlTag.class);
XmlTag html = null;
XmlTag head = null;
if (topLevelTags != null) {
for (XmlTag tag : topLevelTags) {
if ("html".equals(tag.getName())) {
html = tag;
break;
}
if ("head".equals(tag.getName())) {
head = tag;
break;
}
when {
isCSS(file.fileType) -> {
insertTagInHead(target, file, editor, "<link rel={0}stylesheet{0} href={0}{1}{0}>")
return true
}
isJS(file.fileType) -> {
insertTagInHead(target, file, editor, "<script src={0}{1}{0}></script>")
return true
}
isImage(file.fileType) -> {
insertImageTag(target, file, editor, "<img src={0}{1}{0}/>")
return true
}
else -> return false
}
if (html != null) {
head = html.findFirstSubTag("head");
}
final PsiElement parent = head != null ? head : html != null ? html : document;
if (checkIfPresent(editor, tagTemplate, relativePath, parent)) return true;
WriteCommandAction.writeCommandAction(target.getProject(), target).run(() -> {
final XmlTag tag = XmlElementFactory.getInstance(target.getProject()).createHTMLTagFromText(tagText);
if (parent instanceof XmlTag) {
((XmlTag)parent).addSubTag(tag, !"head".equals(((XmlTag)parent).getName()));
}
else {
parent.addAfter(tag, ((XmlDocument)parent).getProlog());
}
});
return true;
}
private static boolean checkIfPresent(Editor editor, String tagTemplate, String relativePath, PsiElement parent) {
for (PsiElement element : parent.getChildren()) {
if (element instanceof XmlTag tag) {
if (tagTemplate.startsWith("<" + tag.getName() + " ")) {
final String attrName = HtmlUtil.SCRIPT_TAG_NAME.equals(tag.getName()) ? "src" : "href";
String path = tag.getAttributeValue(attrName);
if (StringUtil.equals(path, relativePath)) {
final LogicalPosition position = getPosition(editor, tag, attrName);
editor.getScrollingModel().scrollTo(position, ScrollType.MAKE_VISIBLE);
final int flags = HintManager.HIDE_BY_ANY_KEY | HintManager.HIDE_BY_TEXT_CHANGE | HintManager.HIDE_BY_SCROLLING;
final LightweightHint hint = new LightweightHint(HintUtil.createInformationLabel(
HtmlToolsBundle.message("html.drop.handler.hint.text.file.already.linked", relativePath)));
final Point point = HintManagerImpl.getHintPosition(hint, editor, position, HintManager.ABOVE);
HintManagerImpl.getInstanceImpl().showEditorHint(hint, editor, point, flags, 0, false);
return true;
}
}
}
}
return false;
}
@SuppressWarnings("ConstantConditions")
private static LogicalPosition getPosition(Editor editor, XmlTag tag, String attrName) {
final XmlAttributeValue element = tag.getAttribute(attrName).getValueElement();
return editor.offsetToLogicalPosition(element.getTextRange().getStartOffset() + element.getTextRange().getLength() / 2);
}
private static boolean insertImageTag(HtmlFileImpl target, VirtualFile droppedFile, Editor editor, String tagTemplate) {
final String tagText = prepareTagText(target, tagTemplate, getRelativePath(target, droppedFile));
final int offset = editor.getCaretModel().getOffset();
PsiElement element = target.findElementAt(offset);
if (element == null || tagText == null) return false;
final XmlElement parent = PsiTreeUtil.getParentOfType(element, XmlTag.class, XmlDocument.class);
PsiElement candidate = null;
for (PsiElement psiElement : parent.getChildren()) {
if (psiElement.getTextRange().getEndOffset() >= offset) {
candidate = psiElement;
break;
}
}
if (parent instanceof XmlTag && candidate != null) {
final ASTNode startEnd = XmlChildRole.START_TAG_END_FINDER.findChild(parent.getNode());
if (startEnd != null && startEnd.getTextRange().getEndOffset() > candidate.getTextRange().getEndOffset()) {
candidate = startEnd.getPsi();
}
final ASTNode endStart = XmlChildRole.CLOSING_TAG_START_FINDER.findChild(parent.getNode());
if (endStart != null && endStart.getTextRange().getStartOffset() <= candidate.getTextRange().getStartOffset()) {
candidate = endStart.getPsi().getPrevSibling();
}
}
PsiElement insertionPoint = candidate;
WriteCommandAction.writeCommandAction(target.getProject(), target).run(() -> {
XmlTag tag = XmlElementFactory.getInstance(target.getProject()).createHTMLTagFromText(tagText);
tag = (XmlTag)parent.addAfter(tag, insertionPoint);
final ImageInfo info = HtmlReferenceProvider.SizeReference.getImageInfo(tag);
if (info != null && info.width != 0 && info.height != 0) {
tag.setAttribute("height", String.valueOf(info.height));
tag.setAttribute("width", String.valueOf(info.width));
}
});
return true;
}
private static @Nullable String prepareTagText(HtmlFileImpl target, String tagTemplate, String path) {
return path != null ? MessageFormat.format(tagTemplate, XmlEditUtil.getAttributeQuote(target), path) : null;
}
private static String getRelativePath(HtmlFileImpl target, VirtualFile droppedFile) {
final VirtualFile targetFile = target.getVirtualFile();
return FileUtil.getRelativePath(targetFile.getParent().getPath(), droppedFile.getPath(), '/');
}
private static @Nullable VirtualFile getDroppedFile(@NotNull Transferable t) {
List<File> list = FileCopyPasteUtil.getFileList(t);
final File io = list != null && list.size() == 1 ? ContainerUtil.getFirstItem(list) : null;
return io != null ? VfsUtil.findFileByIoFile(io, true) : null;
}
private static boolean isJS(FileType type) {
return type instanceof LanguageFileType && ((LanguageFileType)type).getLanguage().isKindOf(Language.findLanguageByID("JavaScript"));
}
private static boolean isCSS(FileType type) {
return type instanceof LanguageFileType && ((LanguageFileType)type).getLanguage().isKindOf(Language.findLanguageByID("CSS"));
}
private static boolean isImage(FileType type) {
return type == ImageFileTypeManager.getInstance().getImageFileType();
}
}
private suspend fun insertTagInHead(target: HtmlFileImpl, droppedFile: VirtualFile, editor: Editor, tagTemplate: String) {
val (relativePath, parent, existingPosition, tagText) = readAction {
val relativePath = getRelativePath(target, droppedFile)
val tagText = prepareTagText(target, tagTemplate, relativePath)
val document = target.document
if (document == null || tagText == null) return@readAction null
val topLevelTags = PsiTreeUtil.getChildrenOfType(document, XmlTag::class.java)
var html: XmlTag? = null
var head: XmlTag? = null
if (topLevelTags != null) {
for (tag in topLevelTags) {
if ("html" == tag.name) {
html = tag
break
}
if ("head" == tag.name) {
head = tag
break
}
}
}
if (html != null) {
head = html.findFirstSubTag("head")
}
val parent: PsiElement = head ?: (html ?: document)
val existingPosition = findExistingPosition(editor, tagTemplate, relativePath, parent)
InsertTagData(relativePath, parent, existingPosition, tagText)
} ?: return
withContext(Dispatchers.EDT) {
if (editor.isDisposed) return@withContext
if (existingPosition != null) {
editor.scrollingModel.scrollTo(existingPosition, ScrollType.MAKE_VISIBLE)
val flags = HintManager.HIDE_BY_ANY_KEY or HintManager.HIDE_BY_TEXT_CHANGE or HintManager.HIDE_BY_SCROLLING
val hint = LightweightHint(HintUtil.createInformationLabel(HtmlToolsBundle.message("html.drop.handler.hint.text.file.already.linked", relativePath)))
val point = HintManagerImpl.getHintPosition(hint, editor, existingPosition, HintManager.ABOVE)
HintManagerImpl.getInstanceImpl().showEditorHint(hint, editor, point, flags, 0, false)
}
else {
WriteCommandAction.writeCommandAction(target.project, target).run<RuntimeException> {
val tag = XmlElementFactory.getInstance(target.project).createHTMLTagFromText(tagText)
if (parent is XmlTag) {
parent.addSubTag(tag, "head" != parent.name)
}
else {
parent.addAfter(tag, (parent as XmlDocument).prolog)
}
}
}
}
}
private data class InsertTagData(val relativePath: String?,
val parent: PsiElement,
val existingPosition: LogicalPosition?,
val tagText: String)
@RequiresReadLock
private fun findExistingPosition(editor: Editor, tagTemplate: String, relativePath: String?, parent: PsiElement): LogicalPosition? {
for (element in parent.children) {
if (element is XmlTag) {
if (tagTemplate.startsWith("<" + element.name + " ")) {
val attrName = if ((HtmlUtil.SCRIPT_TAG_NAME == element.name)) "src" else "href"
val path = element.getAttributeValue(attrName)
if (StringUtil.equals(path, relativePath)) {
val position = getPosition(editor, element, attrName)
return position
}
}
}
}
return null
}
private fun getPosition(editor: Editor, tag: XmlTag, attrName: String): LogicalPosition {
val element = tag.getAttribute(attrName)!!.valueElement
return editor.offsetToLogicalPosition(element!!.textRange.startOffset + element.textRange.length / 2)
}
private suspend fun insertImageTag(target: HtmlFileImpl, droppedFile: VirtualFile, editor: Editor, tagTemplate: String) {
val (parent, insertionPoint, tagText, imageInfo) = readAction {
val tagText = prepareTagText(target, tagTemplate, getRelativePath(target, droppedFile))
val offset = editor.caretModel.offset
val element = target.findElementAt(offset)
if (element == null || tagText == null) return@readAction null
val parent: XmlElement = PsiTreeUtil.getParentOfType(element, XmlTag::class.java, XmlDocument::class.java) ?: return@readAction null
var candidate: PsiElement? = null
for (psiElement in parent.children) {
if (psiElement.textRange.endOffset >= offset) {
candidate = psiElement
break
}
}
if (parent is XmlTag && candidate != null) {
val startEnd = XmlChildRole.START_TAG_END_FINDER.findChild(parent.getNode())
if (startEnd != null && startEnd.textRange.endOffset > candidate.textRange.endOffset) {
candidate = startEnd.psi
}
val endStart = XmlChildRole.CLOSING_TAG_START_FINDER.findChild(parent.getNode())
if (endStart != null && endStart.textRange.startOffset <= candidate!!.textRange.startOffset) {
candidate = endStart.psi.prevSibling
}
}
val insertionPoint = candidate
val imageInfo = ImageInfoIndex.getInfo(droppedFile, target.getProject())
InsertImageTagData(parent, insertionPoint, tagText, imageInfo)
} ?: return
withContext(Dispatchers.EDT) {
WriteCommandAction.writeCommandAction(target.project, target).run<RuntimeException> {
if (!parent.isValid) return@run
var tag = XmlElementFactory.getInstance(target.project).createHTMLTagFromText(tagText)
tag = parent.addAfter(tag, insertionPoint) as XmlTag
if (imageInfo != null && imageInfo.width != 0 && imageInfo.height != 0) {
tag.setAttribute("height", imageInfo.height.toString())
tag.setAttribute("width", imageInfo.width.toString())
}
}
}
}
data class InsertImageTagData(val parent: XmlElement,
val insertionPoint: PsiElement?,
val tagText: String,
val imageInfo: ImageInfo?)
private fun prepareTagText(target: HtmlFileImpl, tagTemplate: String, path: String?): String? {
return if (path != null) MessageFormat.format(tagTemplate, XmlEditUtil.getAttributeQuote(target), path) else null
}
private fun getRelativePath(target: HtmlFileImpl, droppedFile: VirtualFile): String? {
val targetFile = target.virtualFile
return FileUtil.getRelativePath(targetFile.parent.path, droppedFile.path, '/')
}
private fun getDroppedFile(list: Collection<File>): VirtualFile? {
val io = if (list.size == 1) list.firstOrNull() else null
return if (io != null) VfsUtil.findFileByIoFile(io, true) else null
}
private fun isJS(type: FileType): Boolean {
return type is LanguageFileType && type.language.isKindOf(Language.findLanguageByID("JavaScript"))
}
private fun isCSS(type: FileType): Boolean {
return type is LanguageFileType && type.language.isKindOf(Language.findLanguageByID("CSS"))
}
private fun isImage(type: FileType): Boolean {
return type === ImageFileTypeManager.getInstance().imageFileType
}