[pycharm] PY-63670 - fixes according to IJ-CR-125957, part 1 - without touching async and json parsing part

GitOrigin-RevId: acd02edbe98a47c0147797edd2a2131e1e67074e
This commit is contained in:
Bogdan Kirilenko
2024-03-23 02:13:34 +00:00
committed by intellij-monorepo-bot
parent 9ca2fbfcd5
commit 87f388f7fe
33 changed files with 263 additions and 348 deletions

Before

Width:  |  Height:  |  Size: 716 B

After

Width:  |  Height:  |  Size: 716 B

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

@@ -4,9 +4,7 @@
</dependencies>
<extensions defaultExtensionNs="com.intellij">
<annotator language="Python" implementationClass="com.intellij.python.community.impl.huggingFace.annotation.HuggingFaceEntityNameAnnotator"/>
<psi.referenceContributor implementation="com.intellij.python.community.impl.huggingFace.annotation.HuggingFaceModelReferenceContributor"
language="Python"/>
<psi.referenceContributor implementation="com.intellij.python.community.impl.huggingFace.annotation.HuggingFaceDatasetReferenceContributor"
<psi.referenceContributor implementation="com.intellij.python.community.impl.huggingFace.annotation.HuggingFaceIdentifierReferenceContributor"
language="Python"/>
<platform.backend.documentation.psiTargetProvider id="HuggingFaceDocumentationProvider"
implementation="com.intellij.python.community.impl.huggingFace.documentation.HuggingFaceDocumentationTargetProvider"/>
@@ -4,14 +4,13 @@ python.hugging.face.default.value=Default value
python.hugging.face.unknown.element=Unknown element
python.hugging.face.not.found=Error fetching model data: {0}
python.hugging.face.base.url=https://huggingface.co
python.hugging.face.documentation.hint=Open in browser
updated.0= Updated {0}
python.hugging.face.html.head="Documentation for {0}"
open.on.hugging.face=Open on Hugging Face
python.hugging.face.dictionary.name=Hugging Face model and dataset dictionary
python.hugging.face.updated.suffix= Updated {0}
python.hugging.face.open.on.link.text=Open on Hugging Face
python.hugging.face.placeholder.gated.model=<font color='gray'>Sorry, this {0} is gated.<br>Please visit the [HuggingFace website]({1}) <br>for more details about the {2} card.</font>
python.hugging.face.placeholder.no.readme=This model has no description available. For more details, please follow the [link]({0})
python.hugging.face.placeholder.trimmed=*Please find the rest of the {0} card [here]({1})*
python.hugging.face.placeholder.no.internet=Failed to fetch data for {0}. Please check your internet connection.
python.hugging.face.dataset=Dataset
python.hugging.face.tags.style=<style>{0}</style>
@@ -1,12 +1,14 @@
// 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.python.community.impl.huggingFace
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
object HuggingFaceConstants {
const val HF_API_FETCH_PAGE_SIZE = 500
const val MAX_MODELS_IN_CACHE = HF_API_FETCH_PAGE_SIZE * 20
const val MAX_DATASETS_IN_CACHE = HF_API_FETCH_PAGE_SIZE * 20
const val HF_EMOJI = "\uD83E\uDD17"
const val MAX_MD_CHAR_NUM = 5000
const val API_FETCH_SORT_KEY = "likes"
const val DATASET_FAKE_PIPELINE_TAG = "dataset"
const val UNDEFINED_PIPELINE_TAG = "undefined"
@@ -1,6 +1,9 @@
// 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.python.community.impl.huggingFace
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
enum class HuggingFaceEntityKind(val urlFragment: String, val printName: String) {
MODEL("models", "model"),
DATASET("datasets", "dataset")
@@ -6,8 +6,7 @@ import com.intellij.openapi.roots.ProjectFileIndex
import com.intellij.openapi.util.NlsSafe
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiManager
import com.intellij.python.community.impl.huggingFace.annotation.HuggingFaceDatasetPsiElement
import com.intellij.python.community.impl.huggingFace.annotation.HuggingFaceModelPsiElement
import com.intellij.python.community.impl.huggingFace.annotation.HuggingFaceIdentifierPsiElement
import com.intellij.python.community.impl.huggingFace.api.HuggingFaceApi
import com.intellij.python.community.impl.huggingFace.cache.HuggingFaceDatasetsCache
import com.intellij.python.community.impl.huggingFace.cache.HuggingFaceLastCacheRefresh
@@ -15,16 +14,18 @@ import com.intellij.python.community.impl.huggingFace.cache.HuggingFaceModelsCac
import com.intellij.python.community.impl.huggingFace.service.PyHuggingFaceBundle
import com.jetbrains.python.psi.*
import com.jetbrains.python.psi.impl.PyPlainStringElementImpl
import org.jetbrains.annotations.ApiStatus
import java.time.Duration
val huggingFaceRelevantLibraries = setOf(
"diffusers", "transformers", "allennlp", "spacy",
"asteroid", "flair", "keras", "sentence-transformers",
"stable-baselines3", "adapters", "huggingface_hub",
)
@ApiStatus.Internal
object HuggingFaceUtil {
private val huggingFaceRelevantLibraries = setOf(
"diffusers", "transformers", "allennlp", "spacy",
"asteroid", "flair", "keras", "sentence-transformers",
"stable-baselines3", "adapters", "huggingface_hub",
)
private fun isAnyHFLibraryImportedInFile(file: PyFile): Boolean {
val isDirectlyImported = file.importTargets.any { importStmt ->
huggingFaceRelevantLibraries.any { lib -> importStmt.importedQName.toString().contains(lib) }
@@ -56,7 +57,7 @@ object HuggingFaceUtil {
return isLibraryImported
}
private fun isWhatHuggingFaceEntity(text: String): HuggingFaceEntityKind? {
fun isWhatHuggingFaceEntity(text: String): HuggingFaceEntityKind? {
return when {
isHuggingFaceModel(text) -> HuggingFaceEntityKind.MODEL
isHuggingFaceDataset(text) -> HuggingFaceEntityKind.DATASET
@@ -64,9 +65,9 @@ object HuggingFaceUtil {
}
}
fun isHuggingFaceModel(text: String): Boolean = HuggingFaceModelsCache.isInCache(text)
private fun isHuggingFaceModel(text: String): Boolean = HuggingFaceModelsCache.isInCache(text)
fun isHuggingFaceDataset(text: String): Boolean = HuggingFaceDatasetsCache.isInCache(text)
private fun isHuggingFaceDataset(text: String): Boolean = HuggingFaceDatasetsCache.isInCache(text)
fun isHuggingFaceEntity(text: String): Boolean = isHuggingFaceModel(text) || isHuggingFaceDataset(text)
@@ -81,35 +82,34 @@ object HuggingFaceUtil {
}
}
private fun extractStringAndKindFromStringLiteralExpression(stringValue: String?): Pair<String?, HuggingFaceEntityKind?> {
private fun extractStringAndKindFromStringLiteralExpression(stringValue: String?): Pair<String, HuggingFaceEntityKind>? {
val entityKind = if (stringValue != null) isWhatHuggingFaceEntity(stringValue) else null
return Pair(stringValue, entityKind)
return stringValue?.takeIf { entityKind != null }?.let { Pair(it, entityKind!!) }
}
fun extractStringValueAndEntityKindFromElement(element: PsiElement): Pair<String?, HuggingFaceEntityKind?> {
fun extractStringValueAndEntityKindFromElement(element: PsiElement): Pair<String, HuggingFaceEntityKind>? {
return when (val parent = element.parent) {
is PyAssignmentStatement -> when (element) {
is PyTargetExpression -> {
val stringValue = (parent.assignedValue as? PyStringLiteralExpression)?.stringValue
extractStringAndKindFromStringLiteralExpression(stringValue)
}
else -> Pair(null, null)
else -> null
}
is PyStringLiteralExpression -> when (element) {
is HuggingFaceModelPsiElement -> Pair(parent.stringValue, HuggingFaceEntityKind.MODEL)
is HuggingFaceDatasetPsiElement -> Pair(parent.stringValue, HuggingFaceEntityKind.DATASET)
is HuggingFaceIdentifierPsiElement -> Pair(element.entityName, element.entityKind)
is PyPlainStringElementImpl -> {
extractStringAndKindFromStringLiteralExpression(parent.stringValue)
}
else -> Pair(null, null)
else -> null
}
is PyArgumentList -> when (element) {
is PyStringLiteralExpression -> {
extractStringAndKindFromStringLiteralExpression(element.stringValue)
}
else -> Pair(null, null)
else -> null
}
else -> Pair(null, null)
else -> null
}
}
@@ -127,4 +127,16 @@ object HuggingFaceUtil {
HuggingFaceDatasetsCache,
HuggingFaceConstants.MAX_DATASETS_IN_CACHE)
}
fun humanReadableNumber(rawNumber: Int): String {
if (rawNumber < 1000) return rawNumber.toString()
val suffixes = arrayOf("K", "M", "B", "T")
var count = rawNumber.toDouble()
var i = 0
while (i < suffixes.size && count >= 1000) {
count /= 1000
i++
}
return String.format("%.1f%s", count, suffixes[i - 1])
}
}
@@ -0,0 +1,6 @@
// 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.python.community.impl.huggingFace.annotation
@JvmInline value class HuggingFaceModelId(val id: String)
@JvmInline value class HuggingFaceDatasetId(val id: String)
@JvmInline value class HuggingFaceEntityId(val id: String)
@@ -8,17 +8,18 @@ import com.intellij.openapi.editor.colors.CodeInsightColors
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import com.intellij.python.community.impl.huggingFace.HuggingFaceUtil
import com.intellij.python.community.impl.huggingFace.annotation.HuggingFaceEntityPsiElement.Companion.HUGGING_FACE_ENTITY_NAME_KEY
import com.intellij.python.community.impl.huggingFace.service.HuggingFaceImportedLibrariesManagerService
import com.intellij.python.community.impl.huggingFace.annotation.HuggingFaceIdentifierPsiElement.Companion.HUGGING_FACE_ENTITY_NAME_KEY
import com.intellij.python.community.impl.huggingFace.service.HuggingFaceImportedLibrariesManager
import com.jetbrains.python.psi.PyStringLiteralExpression
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
class HuggingFaceEntityNameAnnotator : Annotator {
override fun annotate(element: PsiElement, holder: AnnotationHolder) {
if (element !is PyStringLiteralExpression) return
val project = element.project
val service = project.getService(HuggingFaceImportedLibrariesManagerService::class.java)
val manager = service.getManager()
val manager = project.getService(HuggingFaceImportedLibrariesManager::class.java)
if (!manager.isLibraryImported()) return
val text = element.stringValue
@@ -36,7 +37,6 @@ class HuggingFaceEntityNameAnnotator : Annotator {
}
private fun isValidHfString(element: PyStringLiteralExpression, text: String): Boolean {
val userData = element.getUserData(HUGGING_FACE_ENTITY_NAME_KEY)
return (userData == true) || HuggingFaceUtil.isHuggingFaceEntity(text)
return (element.getUserData(HUGGING_FACE_ENTITY_NAME_KEY) == true) || HuggingFaceUtil.isHuggingFaceEntity(text)
}
}
@@ -7,15 +7,19 @@ import com.intellij.openapi.util.NlsSafe
import com.intellij.psi.PsiElement
import com.intellij.psi.impl.FakePsiElement
import com.intellij.python.community.impl.huggingFace.HuggingFaceConstants
import com.intellij.python.community.impl.huggingFace.HuggingFaceEntityKind
import com.intellij.python.community.impl.huggingFace.api.HuggingFaceURLProvider
import com.intellij.python.community.impl.huggingFace.cache.HuggingFaceModelsCache
import com.intellij.python.community.impl.huggingFace.service.HuggingFaceCardsUsageCollector
import org.jetbrains.annotations.ApiStatus
abstract class HuggingFaceEntityPsiElement(
@ApiStatus.Internal
class HuggingFaceIdentifierPsiElement(
private val parent: PsiElement,
private val entityName: String
val entityName: String,
val entityKind: HuggingFaceEntityKind
) : FakePsiElement() {
init {
parent.putUserData(HUGGING_FACE_ENTITY_NAME_KEY, true)
}
@@ -25,24 +29,22 @@ abstract class HuggingFaceEntityPsiElement(
@NlsSafe
fun stringValue(): String = entityName
override fun navigate(requestFocus: Boolean) {
when (entityKind) {
HuggingFaceEntityKind.MODEL -> {
val pipelineTag = HuggingFaceModelsCache.getPipelineTagForEntity(entityName) ?:
HuggingFaceConstants.UNDEFINED_PIPELINE_TAG
HuggingFaceCardsUsageCollector.NAVIGATION_LINK_IN_EDITOR_CLICKED.log(pipelineTag)
BrowserUtil.browse(HuggingFaceURLProvider.getModelCardLink(entityName))
}
HuggingFaceEntityKind.DATASET -> {
HuggingFaceCardsUsageCollector.NAVIGATION_LINK_IN_EDITOR_CLICKED.log(HuggingFaceConstants.DATASET_FAKE_PIPELINE_TAG)
BrowserUtil.browse(HuggingFaceURLProvider.getDatasetCardLink(entityName))
}
}
}
companion object {
val HUGGING_FACE_ENTITY_NAME_KEY = Key.create<Boolean>("HUGGING_FACE_ENTITY_NAME_KEY")
}
}
class HuggingFaceModelPsiElement(parent: PsiElement, private val modelName: String
) : HuggingFaceEntityPsiElement(parent, modelName) {
override fun navigate(requestFocus: Boolean) {
val pipelineTag = HuggingFaceModelsCache.getPipelineTagForEntity(modelName) ?: HuggingFaceConstants.UNDEFINED_PIPELINE_TAG
HuggingFaceCardsUsageCollector.NAVIGATION_LINK_IN_EDITOR_CLICKED.log(pipelineTag)
BrowserUtil.browse(HuggingFaceURLProvider.getModelCardLink(modelName))
}
}
class HuggingFaceDatasetPsiElement(parent: PsiElement, private val datasetName: String
) : HuggingFaceEntityPsiElement(parent, datasetName) {
override fun navigate(requestFocus: Boolean) {
HuggingFaceCardsUsageCollector.NAVIGATION_LINK_IN_EDITOR_CLICKED.log(HuggingFaceConstants.DATASET_FAKE_PIPELINE_TAG)
BrowserUtil.browse(HuggingFaceURLProvider.getDatasetCardLink(datasetName))
}
}
@@ -4,23 +4,18 @@ package com.intellij.python.community.impl.huggingFace.annotation
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiReferenceBase
import com.intellij.python.community.impl.huggingFace.HuggingFaceEntityKind
import org.jetbrains.annotations.ApiStatus
class HuggingFaceModelReference(
@ApiStatus.Internal
class HuggingFaceReference(
element: PsiElement,
textRange: TextRange,
private val modelName: String
private val identifier: String,
private val entityKind: HuggingFaceEntityKind,
) : PsiReferenceBase<PsiElement>(element, textRange) {
override fun resolve(): PsiElement {
return HuggingFaceModelPsiElement(myElement, modelName)
}
}
class HuggingFaceDatasetReference(
element: PsiElement,
textRange: TextRange,
private val datasetName: String
) : PsiReferenceBase<PsiElement>(element, textRange) {
override fun resolve(): PsiElement {
return HuggingFaceDatasetPsiElement(myElement, datasetName)
return HuggingFaceIdentifierPsiElement(myElement, identifier, entityKind)
}
}
@@ -5,23 +5,14 @@ import com.intellij.patterns.PlatformPatterns
import com.intellij.psi.PsiReferenceContributor
import com.intellij.psi.PsiReferenceRegistrar
import com.jetbrains.python.PyElementTypes
import org.jetbrains.annotations.ApiStatus
class HuggingFaceModelReferenceContributor: PsiReferenceContributor() {
@ApiStatus.Internal
class HuggingFaceIdentifierReferenceContributor: PsiReferenceContributor() {
override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) {
registrar.registerReferenceProvider(
PlatformPatterns.psiElement(PyElementTypes.STRING_LITERAL_EXPRESSION),
HuggingFaceModelReferenceProvider(),
PsiReferenceRegistrar.DEFAULT_PRIORITY
)
}
}
class HuggingFaceDatasetReferenceContributor: PsiReferenceContributor() {
override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) {
registrar.registerReferenceProvider(
PlatformPatterns.psiElement(PyElementTypes.STRING_LITERAL_EXPRESSION),
HuggingFaceDatasetReferenceProvider(),
HuggingFaceIdentifierReferenceProvider(),
PsiReferenceRegistrar.DEFAULT_PRIORITY
)
}
@@ -5,32 +5,28 @@ import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiReference
import com.intellij.psi.PsiReferenceProvider
import com.intellij.python.community.impl.huggingFace.service.HuggingFaceImportedLibrariesManagerService
import com.intellij.python.community.impl.huggingFace.HuggingFaceUtil
import com.intellij.python.community.impl.huggingFace.service.HuggingFaceImportedLibrariesManager
import com.intellij.util.ProcessingContext
import com.jetbrains.python.psi.PyStringLiteralExpression
import org.jetbrains.annotations.ApiStatus
abstract class HuggingFaceReferenceProvider : PsiReferenceProvider() {
@ApiStatus.Internal
class HuggingFaceIdentifierReferenceProvider : PsiReferenceProvider() {
override fun getReferencesByElement(element: PsiElement, context: ProcessingContext): Array<PsiReference> {
val pyStringLiteralExpression = element as? PyStringLiteralExpression ?: return PsiReference.EMPTY_ARRAY
val project = element.project
val service = project.getService(HuggingFaceImportedLibrariesManagerService::class.java)
val manager = service.getManager()
val manager = project.getService(HuggingFaceImportedLibrariesManager::class.java)
if (!manager.isLibraryImported()) return PsiReference.EMPTY_ARRAY
val text = pyStringLiteralExpression.stringValue
if (!isValidReference(text)) return PsiReference.EMPTY_ARRAY
val entityKind = HuggingFaceUtil.isWhatHuggingFaceEntity(text) ?: return PsiReference.EMPTY_ARRAY
val textRange = getTextRange(element, text)
return getReferenceArray(element, textRange, text)
return arrayOf(HuggingFaceReference(element, textRange, text, entityKind))
}
abstract fun isValidReference(text: String) : Boolean
abstract fun getReferenceArray(element: PsiElement, textRange: TextRange, text: String) : Array<PsiReference>
private fun getTextRange(element: PsiElement, text: String): TextRange {
val startOffset = element.text.indexOf(text)
return if (startOffset >= 0) {
@@ -40,17 +36,3 @@ abstract class HuggingFaceReferenceProvider : PsiReferenceProvider() {
}
}
}
class HuggingFaceModelReferenceProvider : HuggingFaceReferenceProvider() {
override fun isValidReference(text: String) = HuggingFaceUtil.isHuggingFaceModel(text)
override fun getReferenceArray(element: PsiElement, textRange: TextRange, text: String): Array<PsiReference> =
arrayOf(HuggingFaceModelReference(element, textRange, text))
}
class HuggingFaceDatasetReferenceProvider : HuggingFaceReferenceProvider() {
override fun isValidReference(text: String) = HuggingFaceUtil.isHuggingFaceDataset(text)
override fun getReferenceArray(element: PsiElement, textRange: TextRange, text: String): Array<PsiReference> =
arrayOf(HuggingFaceDatasetReference(element, textRange, text))
}
@@ -8,9 +8,18 @@ import com.intellij.openapi.util.NlsSafe
import com.intellij.python.community.impl.huggingFace.HuggingFaceConstants
import com.intellij.python.community.impl.huggingFace.HuggingFaceEntityKind
import com.intellij.python.community.impl.huggingFace.cache.HuggingFaceCache
import com.intellij.python.community.impl.huggingFace.cache.HuggingFaceMdCacheEntry
import com.intellij.python.community.impl.huggingFace.cache.HuggingFaceMdCardsCache
import com.intellij.python.community.impl.huggingFace.documentation.HuggingFaceDocumentationPlaceholdersUtil
import com.intellij.python.community.impl.huggingFace.documentation.HuggingFaceReadmeCleaner
import com.intellij.python.community.impl.huggingFace.service.HuggingFaceSafeExecutor
import org.jetbrains.annotations.ApiStatus
import java.time.Instant
@ApiStatus.Internal
object HuggingFaceApi {
private val nextLinkRegex = Regex("""<(.+)>; rel="next"""")
fun fillCacheWithBasicApiData(endpoint: HuggingFaceEntityKind, cache: HuggingFaceCache, maxCount: Int) {
val executor = HuggingFaceSafeExecutor.instance
@@ -28,8 +37,8 @@ object HuggingFaceApi {
}
}
suspend fun performSearch(query: String, tags: String?): Map<String, HuggingFaceEntityBasicApiData> {
val queryUrl = HuggingFaceURLProvider.getModelSearchQuery(query, tags)
suspend fun performSearch(query: String, tags: String?, sortKey: HuggingFaceModelSortKey): Map<String, HuggingFaceEntityBasicApiData> {
val queryUrl = HuggingFaceURLProvider.getModelSearchQuery(query, tags, sortKey)
val executor = HuggingFaceSafeExecutor.instance
return executor.asyncSuspend("SearchHuggingFace") {
@@ -44,7 +53,7 @@ object HuggingFaceApi {
}
private fun extractNextPageUrl(linkHeader: String?): String? {
val nextLinkMatch = Regex("""<(.+)>; rel="next"""").find(linkHeader ?: "")
val nextLinkMatch = nextLinkRegex.find(linkHeader ?: "")
return nextLinkMatch?.groupValues?.get(1)
}
@@ -83,4 +92,28 @@ object HuggingFaceApi {
return modelDataMap
}
fun fetchOrRetrieveModelCard(entityDataApiContent: HuggingFaceEntityBasicApiData,
entityId: String,
entityKind: HuggingFaceEntityKind,
prefix: String = "markdown"): String {
return if (entityDataApiContent.gated != "false") {
HuggingFaceDocumentationPlaceholdersUtil.generateGatedEntityMarkdownString(entityId, entityKind)
}
else {
val cached = HuggingFaceMdCardsCache.getData("${prefix}_${entityId}")
cached?.data
?: try {
val mdUrl = HuggingFaceURLProvider.getEntityMarkdownURL(entityId, entityKind).toString()
val rawData = HuggingFaceHttpClient.downloadFile(mdUrl)
?: HuggingFaceDocumentationPlaceholdersUtil.noInternetConnectionPlaceholder(entityId)
val cleanedData = HuggingFaceReadmeCleaner(rawData, entityId, entityKind).doCleanUp().getMarkdown()
HuggingFaceMdCardsCache.saveData("markdown_$entityId", HuggingFaceMdCacheEntry(cleanedData, Instant.now()))
cleanedData
}
catch (e: Exception) {
HuggingFaceDocumentationPlaceholdersUtil.noInternetConnectionPlaceholder(entityId)
}
}
}
}
@@ -3,12 +3,15 @@ package com.intellij.python.community.impl.huggingFace.api
import com.intellij.openapi.util.NlsSafe
import com.intellij.python.community.impl.huggingFace.HuggingFaceEntityKind
import com.intellij.python.community.impl.huggingFace.HuggingFaceUtil.humanReadableNumber
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nls
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
@ApiStatus.Internal
data class HuggingFaceEntityBasicApiData (
val kind: HuggingFaceEntityKind,
@Nls val itemId: String,
@@ -19,31 +22,9 @@ data class HuggingFaceEntityBasicApiData (
val libraryName: String?,
@NlsSafe val pipelineTag: String
) {
@Nls
private fun humanReadableNumber(rawNumber: Int): String {
if (rawNumber < 1000) return rawNumber.toString()
val suffixes = arrayOf("K", "M", "B", "T")
var count = rawNumber.toDouble()
var i = 0
while (i < suffixes.size && count >= 1000) {
count /= 1000
i++
}
return String.format("%.1f%s", count, suffixes[i - 1])
}
@Nls
fun humanReadableLikes(): String {
return humanReadableNumber(likes)
}
@Nls
fun humanReadableDownloads(): String {
return humanReadableNumber(downloads)
}
fun humanReadableLastUpdated(): String {
val parsedDate = LocalDateTime.parse(lastModified, DateTimeFormatter.ISO_DATE_TIME)
return parsedDate.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM))
}
val humanReadableLikes: String = humanReadableNumber(likes)
val humanReadableDownloads: String = humanReadableNumber(downloads)
val humanReadableLastUpdated: String =
LocalDateTime.parse(lastModified, DateTimeFormatter.ISO_DATE_TIME)
.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM))
}
@@ -3,12 +3,13 @@ package com.intellij.python.community.impl.huggingFace.api
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.util.io.HttpRequests
import org.jetbrains.annotations.ApiStatus
import java.net.HttpURLConnection
import java.net.URL
data class HfHttpResponseWithHeaders(val content: String?, val linkHeader: String?)
@ApiStatus.Internal data class HfHttpResponseWithHeaders(val content: String?, val linkHeader: String?)
@ApiStatus.Internal
object HuggingFaceHttpClient {
fun exists(url: String, retries: Int = 2): Boolean {
repeat(retries) {
@@ -0,0 +1,13 @@
// 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.python.community.impl.huggingFace.api
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
enum class HuggingFaceModelSortKey(val value: String, val displayName: String) {
AUTHOR("author", "Author"),
CREATED_AT("createdAt", "Created At"),
LAST_MODIFIED("lastModified", "Last Modified"),
DOWNLOADS("downloads", "Downloads"),
LIKES("likes", "Likes");
}
@@ -4,13 +4,15 @@ package com.intellij.python.community.impl.huggingFace.api
import com.intellij.python.community.impl.huggingFace.HuggingFaceConstants
import com.intellij.python.community.impl.huggingFace.HuggingFaceEntityKind
import com.intellij.python.community.impl.huggingFace.service.PyHuggingFaceBundle
import org.jetbrains.annotations.ApiStatus
import java.net.URL
@ApiStatus.Internal
object HuggingFaceURLProvider {
private val baseURL = PyHuggingFaceBundle.message("python.hugging.face.base.url")
private val models_expand_parameters = listOf("gated", "downloads", "likes", "lastModified", "pipeline_tag")
private val datasets_expand_parameters = listOf("gated", "downloads", "likes", "lastModified")
private val modelsExpandParameters = listOf("gated", "downloads", "likes", "lastModified", "pipeline_tag")
private val datasetsExpandParameters = listOf("gated", "downloads", "likes", "lastModified")
fun getEntityMarkdownURL(entityId: String, entityKind: HuggingFaceEntityKind): URL {
return when(entityKind) {
@@ -30,29 +32,24 @@ object HuggingFaceURLProvider {
fun getDatasetCardLink(datasetId: String): URL = URL("$baseURL/datasets/$datasetId")
fun getModelApiLink(modelId: String): URL = URL("$baseURL/api/models/$modelId")
fun getModelSearchQuery(query: String, tags: String? = null, sort: String = "downloads", limit: Int = 20): URL {
return URL("$baseURL/api/models?search=$query&sort=$sort&limit=$limit&direction=-1"
).let { url ->
tags?.let { URL("${url}&filter=${tags}") } ?: url
}
fun getModelSearchQuery(query: String,
tags: String? = null,
sortKey: HuggingFaceModelSortKey = HuggingFaceModelSortKey.DOWNLOADS,
limit: Int = 20
): URL {
val urlString = "$baseURL/api/models?search=$query&sort=${sortKey.value}&limit=$limit&direction=-1&expand[]=gated&expand[]=downloads&expand[]=likes&expand[]=lastModified&expand[]=pipeline_tag&expand=library_name"
return URL(urlString + if (tags.isNullOrBlank()) "" else "&filter=$tags")
}
// Will be necessary to fetch detailed information for dataset
// fun getDatasetApiLink(datasetId: String): URL = URL("$baseURL/api/models/$datasetId")
// may be needed for dataset viewer button:
// fun getDatasetViewerLink(datasetName: String): URL = URL("$baseURL/datasets/$datasetName/viewer")
fun fetchApiDataUrl(
entityKind: HuggingFaceEntityKind,
limit: Int = HuggingFaceConstants.HF_API_FETCH_PAGE_SIZE,
sort: String = HuggingFaceConstants.API_FETCH_SORT_KEY
): URL {
val expand = if (entityKind == HuggingFaceEntityKind.MODEL) {
models_expand_parameters
modelsExpandParameters
} else {
datasets_expand_parameters
datasetsExpandParameters
}
val parameters = mapOf(
@@ -73,9 +70,6 @@ object HuggingFaceURLProvider {
return url
}
fun makeAbsoluteImageLink(entityId: String, relativeImagePath: String): URL =
URL("$baseURL/$entityId/resolve/main/$relativeImagePath")
fun makeAbsoluteFileLink(entityId: String, relativeFilePath: String): URL =
URL("$baseURL/$entityId/blob/main/$relativeFilePath")
}
@@ -7,22 +7,21 @@ import com.intellij.openapi.components.Storage
import com.intellij.python.community.impl.huggingFace.HuggingFaceConstants
import com.intellij.python.community.impl.huggingFace.api.HuggingFaceEntityBasicApiData
import com.intellij.util.xmlb.XmlSerializerUtil
import org.jetbrains.annotations.ApiStatus
@State(
name = "HuggingFaceCache",
storages = [Storage("HuggingFaceCache.xml")]
)
@ApiStatus.Internal
abstract class HuggingFaceCache(private val maxSize: Int) : PersistentStateComponent<HuggingFaceCache> {
private var nameSet: MutableSet<String> = mutableSetOf()
private val hotCacheMaxSize = 32
private var cacheMap: LinkedHashMap<String, HuggingFaceEntityBasicApiData> =
object : LinkedHashMap<String, HuggingFaceEntityBasicApiData>(maxSize, 0.75f, true) {
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, HuggingFaceEntityBasicApiData>?): Boolean {
return size > maxSize
}
}
private var nameSet: MutableSet<String> = mutableSetOf()
private val hotCacheMaxSize = 50 // smaller cache for IDs met on practice
private val hotCache: LinkedHashMap<String, HuggingFaceEntityBasicApiData> =
object : LinkedHashMap<String, HuggingFaceEntityBasicApiData>(16, 0.75f, true) {
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, HuggingFaceEntityBasicApiData>?): Boolean {
@@ -30,6 +29,7 @@ abstract class HuggingFaceCache(private val maxSize: Int) : PersistentStateCompo
}
}
@Synchronized
fun saveEntities(entitiesMap: Map<String, HuggingFaceEntityBasicApiData>) {
cacheMap.putAll(entitiesMap)
entitiesMap.keys.forEach { key ->
@@ -37,6 +37,7 @@ abstract class HuggingFaceCache(private val maxSize: Int) : PersistentStateCompo
}
}
@Synchronized
fun isInCache(entityId: String): Boolean {
if (hotCache.containsKey(entityId)) return true
@@ -46,16 +47,18 @@ abstract class HuggingFaceCache(private val maxSize: Int) : PersistentStateCompo
} else false
}
@Synchronized
fun getBasicData(entityId: String): HuggingFaceEntityBasicApiData? = cacheMap[entityId]
@Synchronized
fun getPipelineTagForEntity(entityId: String): String? {
hotCache[entityId]?.let { return it.pipelineTag }
return cacheMap[entityId]?.pipelineTag
}
override fun getState(): HuggingFaceCache = this
@Synchronized
fun getCacheSize() = cacheMap.size
override fun getState(): HuggingFaceCache = this
override fun loadState(state: HuggingFaceCache) {
XmlSerializerUtil.copyBean(state, this)
@@ -1,12 +1,14 @@
// 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.python.community.impl.huggingFace.cache
import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.util.xmlb.XmlSerializerUtil
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
@Service(Service.Level.PROJECT)
@State(name = "HuggingFaceLastCacheRefresh", storages = [Storage("HuggingFaceLastCacheRefresh.xml")])
class HuggingFaceLastCacheRefresh : PersistentStateComponent<HuggingFaceLastCacheRefresh> {
@@ -5,11 +5,13 @@ import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.util.xmlb.XmlSerializerUtil
import org.jetbrains.annotations.ApiStatus
import java.time.Instant
data class HuggingFaceMdCacheEntry(val data: String, val timeFetched: Instant)
@ApiStatus.Internal data class HuggingFaceMdCacheEntry(val data: String, val timeFetched: Instant)
@ApiStatus.Internal
@State(name = "HuggingFaceMdCardsCache", storages = [Storage("HuggingFaceMdCardsCache.xml")])
object HuggingFaceMdCardsCache: PersistentStateComponent<HuggingFaceMdCardsCache> {
@@ -4,7 +4,9 @@ package com.intellij.python.community.impl.huggingFace.documentation
import com.intellij.python.community.impl.huggingFace.HuggingFaceEntityKind
import com.intellij.python.community.impl.huggingFace.api.HuggingFaceURLProvider
import com.intellij.python.community.impl.huggingFace.service.PyHuggingFaceBundle
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
object HuggingFaceDocumentationPlaceholdersUtil {
fun generateGatedEntityMarkdownString(entityId: String, entityKind: HuggingFaceEntityKind): String {
val msg = PyHuggingFaceBundle.message(
@@ -21,14 +23,6 @@ object HuggingFaceDocumentationPlaceholdersUtil {
return msg
}
fun trimmedMdPlaceholder(entityId: String, entityKind: HuggingFaceEntityKind): String {
val cardUrl = HuggingFaceURLProvider.getEntityCardLink(entityId, entityKind)
val msg = PyHuggingFaceBundle.message("python.hugging.face.placeholder.trimmed", entityKind.printName, cardUrl)
return msg
}
fun noInternetConnectionPlaceholder(entityId: String): String {
val msg = PyHuggingFaceBundle.message("python.hugging.face.placeholder.no.internet", entityId)
return msg
}
fun noInternetConnectionPlaceholder(entityId: String): String =
PyHuggingFaceBundle.message("python.hugging.face.placeholder.no.internet", entityId)
}
@@ -11,51 +11,29 @@ import com.intellij.psi.PsiElement
import com.intellij.python.community.impl.huggingFace.HuggingFaceConstants
import com.intellij.python.community.impl.huggingFace.HuggingFaceEntityKind
import com.intellij.python.community.impl.huggingFace.HuggingFaceUtil
import com.intellij.python.community.impl.huggingFace.annotation.HuggingFaceDatasetPsiElement
import com.intellij.python.community.impl.huggingFace.annotation.HuggingFaceModelPsiElement
import com.intellij.python.community.impl.huggingFace.api.HuggingFaceEntityBasicApiData
import com.intellij.python.community.impl.huggingFace.api.HuggingFaceHttpClient
import com.intellij.python.community.impl.huggingFace.api.HuggingFaceURLProvider
import com.intellij.python.community.impl.huggingFace.annotation.HuggingFaceIdentifierPsiElement
import com.intellij.python.community.impl.huggingFace.api.HuggingFaceApi.fetchOrRetrieveModelCard
import com.intellij.python.community.impl.huggingFace.cache.HuggingFaceDatasetsCache
import com.intellij.python.community.impl.huggingFace.cache.HuggingFaceMdCacheEntry
import com.intellij.python.community.impl.huggingFace.cache.HuggingFaceMdCardsCache
import com.intellij.python.community.impl.huggingFace.cache.HuggingFaceModelsCache
import com.intellij.python.community.impl.huggingFace.service.HuggingFaceCardsUsageCollector
import com.intellij.python.community.impl.huggingFace.service.PyHuggingFaceBundle
import com.intellij.refactoring.suggested.createSmartPointer
import com.jetbrains.python.psi.PyTargetExpression
import java.time.Instant
internal class HuggingFaceDocumentationTarget(private val myElement : PsiElement) : DocumentationTarget {
override fun createPointer(): Pointer<out DocumentationTarget> {
val psiElementPointer = myElement.createSmartPointer()
return object : Pointer<DocumentationTarget> {
override fun dereference(): DocumentationTarget? {
return psiElementPointer.element?.let { HuggingFaceDocumentationTarget(it) }
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as HuggingFaceDocumentationTarget
return myElement == other.myElement
}
override fun hashCode(): Int {
return myElement.hashCode()
}
}
return Pointer<DocumentationTarget> { psiElementPointer.element?.let { HuggingFaceDocumentationTarget(it) } }
}
override fun computePresentation(): TargetPresentation {
val elementText = when (myElement) {
is PyTargetExpression -> "${HuggingFaceConstants.HF_EMOJI} ${HuggingFaceUtil.extractTextFromPyTargetExpression(myElement)}"
is HuggingFaceModelPsiElement -> "${HuggingFaceConstants.HF_EMOJI} ${myElement.stringValue()}"
is HuggingFaceDatasetPsiElement -> "${HuggingFaceConstants.HF_EMOJI} ${myElement.stringValue()}"
is HuggingFaceIdentifierPsiElement -> "${HuggingFaceConstants.HF_EMOJI} ${myElement.stringValue()}"
else -> PyHuggingFaceBundle.message("python.hugging.face.unknown.element")
}
// todo: add HF logo icon here once it is incorporated
return TargetPresentation.builder(elementText).icon(null).presentation()
}
@@ -63,9 +41,7 @@ internal class HuggingFaceDocumentationTarget(private val myElement : PsiElement
override fun computeDocumentation(): DocumentationResult = DocumentationResult.asyncDocumentation {
val (entityId, entityKind) = HuggingFaceUtil.extractStringValueAndEntityKindFromElement(myElement)
if (entityId == null || entityKind == null) {
return@asyncDocumentation DocumentationResult.documentation(PyHuggingFaceBundle.message("python.hugging.face.no.string.value.found"))
}
?: return@asyncDocumentation DocumentationResult.documentation(PyHuggingFaceBundle.message("python.hugging.face.no.string.value.found"))
try {
val entityDataApiContent = if (entityKind == HuggingFaceEntityKind.MODEL) {
@@ -73,7 +49,7 @@ internal class HuggingFaceDocumentationTarget(private val myElement : PsiElement
} else {
HuggingFaceDatasetsCache.getBasicData(entityId)
}
// shall we log failures?
if (entityDataApiContent == null) { return@asyncDocumentation DocumentationResult.documentation(PyHuggingFaceBundle.message("python.hugging.face.could.not.fetch")) }
val modelCardContent = fetchOrRetrieveModelCard(entityDataApiContent, entityId, entityKind)
val project = readAction { myElement.project }
@@ -96,25 +72,4 @@ internal class HuggingFaceDocumentationTarget(private val myElement : PsiElement
DocumentationResult.documentation(PyHuggingFaceBundle.message("python.hugging.face.not.found", e.message ?: ""))
}
}
private fun fetchOrRetrieveModelCard(entityDataApiContent: HuggingFaceEntityBasicApiData,
entityId: String,
entityKind: HuggingFaceEntityKind): String {
return if (entityDataApiContent.gated != "false") {
HuggingFaceDocumentationPlaceholdersUtil.generateGatedEntityMarkdownString(entityId, entityKind)
} else {
val cached = HuggingFaceMdCardsCache.getData("markdown_$entityId")
cached?.data
?: try {
val mdUrl = HuggingFaceURLProvider.getEntityMarkdownURL(entityId, entityKind).toString()
val rawData = HuggingFaceHttpClient.downloadFile(mdUrl)
?: HuggingFaceDocumentationPlaceholdersUtil.noInternetConnectionPlaceholder(entityId)
val cleanedData = HuggingFaceReadmeCleaner(rawData, entityId, entityKind).doCleanUp().getMarkdown()
HuggingFaceMdCardsCache.saveData("markdown_$entityId", HuggingFaceMdCacheEntry(cleanedData, Instant.now()))
cleanedData
} catch (e: Exception) {
HuggingFaceDocumentationPlaceholdersUtil.noInternetConnectionPlaceholder(entityId)
}
}
}
}
@@ -5,34 +5,33 @@ import com.intellij.platform.backend.documentation.DocumentationTarget
import com.intellij.platform.backend.documentation.PsiDocumentationTargetProvider
import com.intellij.psi.PsiElement
import com.intellij.python.community.impl.huggingFace.HuggingFaceUtil
import com.intellij.python.community.impl.huggingFace.annotation.HuggingFaceDatasetPsiElement
import com.intellij.python.community.impl.huggingFace.annotation.HuggingFaceModelPsiElement
import com.intellij.python.community.impl.huggingFace.annotation.HuggingFaceIdentifierPsiElement
import com.jetbrains.python.psi.PyStringLiteralExpression
import com.jetbrains.python.psi.PyTargetExpression
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
class HuggingFaceDocumentationTargetProvider : PsiDocumentationTargetProvider {
override fun documentationTargets(element: PsiElement, originalElement: PsiElement?): List<DocumentationTarget> {
val documentationTargets = mutableListOf<DocumentationTarget>()
if (element is PyTargetExpression) {
val referencedElement = element.findAssignedValue()
if (referencedElement is PyStringLiteralExpression) {
val stringValue = referencedElement.stringValue
if (HuggingFaceUtil.isHuggingFaceEntity(stringValue)) {
when (element) {
is PyTargetExpression -> {
val referencedElement = element.findAssignedValue()
if (referencedElement is PyStringLiteralExpression
&& HuggingFaceUtil.isHuggingFaceEntity(referencedElement.stringValue)) {
documentationTargets.add(HuggingFaceDocumentationTarget(element))
}
}
} else if (element is HuggingFaceModelPsiElement) {
documentationTargets.add(HuggingFaceDocumentationTarget(element))
} else if (element is HuggingFaceDatasetPsiElement) {
documentationTargets.add(HuggingFaceDocumentationTarget(element))
} else if (element is PyStringLiteralExpression) {
val stringValue = element.stringValue
if (HuggingFaceUtil.isHuggingFaceEntity(stringValue)) {
is HuggingFaceIdentifierPsiElement -> {
documentationTargets.add(HuggingFaceDocumentationTarget(element))
}
is PyStringLiteralExpression -> {
if (HuggingFaceUtil.isHuggingFaceEntity(element.stringValue)) {
documentationTargets.add(HuggingFaceDocumentationTarget(element))
}
}
}
return documentationTargets
@@ -10,8 +10,10 @@ import com.intellij.python.community.impl.huggingFace.HuggingFaceEntityKind
import com.intellij.python.community.impl.huggingFace.api.HuggingFaceEntityBasicApiData
import com.intellij.python.community.impl.huggingFace.api.HuggingFaceURLProvider
import com.intellij.python.community.impl.huggingFace.service.PyHuggingFaceBundle
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
class HuggingFaceHtmlBuilder(
private val project: Project,
private val modelDataApiContent: HuggingFaceEntityBasicApiData,
@@ -19,9 +21,13 @@ class HuggingFaceHtmlBuilder(
private val entityKind: HuggingFaceEntityKind
) {
@NlsSafe
suspend fun build(): String {
suspend fun build(noHeader: Boolean = false): String {
val headChunk = HtmlChunk.tag("head").child(HuggingFaceQuickDocStyles.styleChunk())
val cardHeaderChunk = generateCardHeader(modelDataApiContent)
val cardHeaderChunk = if (noHeader) {
HtmlChunk.empty()
} else {
generateCardHeader(modelDataApiContent)
}
@NlsSafe val convertedHtml = readAction { HuggingFaceMarkdownToHtmlConverter(project).convert(modelCardContent) }
val wrappedBodyContent = HtmlChunk.div()
@@ -54,23 +60,21 @@ class HuggingFaceHtmlBuilder(
val modelInfoRow = DocumentationMarkup.GRAYED_ELEMENT
.children(
*conditionalChunks.toTypedArray(),
HtmlChunk.text(PyHuggingFaceBundle.message("updated.0", modelInfo.humanReadableLastUpdated())),
HtmlChunk.text(PyHuggingFaceBundle.message("python.hugging.face.updated.suffix", modelInfo.humanReadableLastUpdated)),
HtmlChunk.nbsp(2),
DOWNLOADS_ICON,
HtmlChunk.raw(HuggingFaceQuickDocStyles.HAIR_SPACE),
HtmlChunk.text(modelInfo.humanReadableDownloads()),
HtmlChunk.text(modelInfo.humanReadableDownloads),
HtmlChunk.nbsp(2),
LIKES_ICON,
HtmlChunk.raw(HuggingFaceQuickDocStyles.HAIR_SPACE),
HtmlChunk.raw(modelInfo.humanReadableLikes()),
HtmlChunk.raw(modelInfo.humanReadableLikes),
HtmlChunk.nbsp(),
)
val linkRow = HtmlChunk.tag("a")
.attr("href", HuggingFaceURLProvider.getEntityCardLink(modelInfo.itemId, entityKind).toString())
.child(HtmlChunk.text(PyHuggingFaceBundle.getMessage("open.on.hugging.face")))
.child(HtmlChunk.text(PyHuggingFaceBundle.getMessage("python.hugging.face.open.on.link.text")))
.wrapWith("p")
val headerContainer = HtmlChunk.div()
@@ -85,12 +89,8 @@ class HuggingFaceHtmlBuilder(
companion object {
private val DOWNLOADS_ICON = HtmlChunk.tag("icon")
// .attr("src", "com.intellij.python.community.impl.huggingFace.icons.PythonCommunityImplHuggingFaceIcons.Download")
.attr("src", "AllIcons.Actions.Download")
private val LIKES_ICON = HtmlChunk.tag("icon")
// .attr(".src", "com.intellij.python.community.impl.huggingFace.icons.PythonCommunityImplHuggingFaceIcons.Like")
.attr(".src", "AllIcons.Toolwindows.ToolWindowFavorites")
//private val LOGO_ICON = HtmlChunk.tag("icon")
// .attr(".src", "/com/intellij/python/community/impl/huggingFace/icons/logo.svg")
}
}
@@ -1,7 +1,7 @@
// 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.python.community.impl.huggingFace.documentation
import com.intellij.json.JsonLanguage
import com.intellij.json.json5.Json5Language
import com.intellij.lang.documentation.DocumentationMarkup.CLASS_SECTION
import com.intellij.lang.documentation.DocumentationMarkup.CLASS_SECTIONS
import com.intellij.markdown.utils.convertMarkdownToHtml
@@ -10,6 +10,7 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.util.text.StringUtil
import com.jetbrains.python.PythonLanguage
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nls
@@ -17,25 +18,23 @@ import org.jetbrains.annotations.Nls
* Justification for the complexity of the HuggingFaceMarkdownToHtmlConverter class:
* it is driven by the unique rendering needs within the quickDoc display context.
* Specifically, pre-tags were enhanced with a subclass to ensure they adapt to the quickDoc window width.
* Similarly, processing for blockquotes and tables was customized to respect the display area's constraints.
* Similarly, processing for blockquotes and tables was customized to respect the display area's constraints.
* These adjustments guarantee that the converted HTML content seamlessly fits within the quickDoc environment,
* enhancing readability and user interaction.
*/
@ApiStatus.Internal
class HuggingFaceMarkdownToHtmlConverter(private val project: Project) {
@NlsSafe
@Nls
fun convert(markdown: String): String {
val convertedHtml = convertMarkdownToHtml(markdown)
val htmlWithoutBaseTags = convertedHtml.replace(
Regex("<(/)?(html|head|body)( [^>]*)?>"),
""
)
val htmlWithoutBaseTags = convertedHtml.replace(HTML_BASE_TAGS_REGEX, "")
val htmlWithFixedPreTags = htmlWithoutBaseTags.replace(
Regex("<pre>"),
PRE_TAG_REGEX,
"<pre class=${HuggingFaceQuickDocStyles.HF_PRE_TAG_CLASS}>"
)
val htmlWithFixedPTags = htmlWithFixedPreTags.replace(
Regex("<p>"),
P_TAG_REGEX,
"<p class=${HuggingFaceQuickDocStyles.HF_P_TAG_CLASS}>"
)
val htmlWithFixedQuotes = addDivToBlockquotes(htmlWithFixedPTags)
@@ -45,21 +44,19 @@ class HuggingFaceMarkdownToHtmlConverter(private val project: Project) {
}
private fun addDivToBlockquotes(html: String): String {
val blockquoteRegex = Regex("<blockquote>(.*?)</blockquote>", RegexOption.DOT_MATCHES_ALL)
return html.replace(blockquoteRegex) {
return html.replace(BLOCKQUOTE_REGEX) {
"<blockquote><div class=\"${HuggingFaceQuickDocStyles.QUOTE_CLASS}\">${it.groupValues[1]}</div></blockquote>"
}
}
private fun formatTablesInHtml(html: String): String {
val htmlNoIntellijRowEven = html.replace(" class=\"intellij-row-even\"", "")
val tableRegex = Regex("<table.*?>(.*?)</table>", RegexOption.DOT_MATCHES_ALL)
val tdRegex = Regex("<td>(.*?)</td>", RegexOption.DOT_MATCHES_ALL)
return htmlNoIntellijRowEven.replace(tableRegex) { tableMatch ->
return htmlNoIntellijRowEven.replace(TABLE_REGEX) { tableMatch ->
val formattedTable = "<table class='$CLASS_SECTIONS'>${tableMatch.groupValues[1]}</table>"
formattedTable.replace(tdRegex) { tdMatch ->
formattedTable.replace(TD_REGEX) { tdMatch ->
"<td valign='top' class='$CLASS_SECTION'>${tdMatch.groupValues[1]}</td>"
}
}
@@ -78,7 +75,7 @@ class HuggingFaceMarkdownToHtmlConverter(private val project: Project) {
val language = when {
// PY-70540 - if additional languages are needed - to be added here
"language-python" in codeAttributes -> PythonLanguage.INSTANCE
"language-json" in codeAttributes -> JsonLanguage.INSTANCE
"language-json" in codeAttributes -> Json5Language.INSTANCE
else -> null
}
@@ -109,4 +106,13 @@ class HuggingFaceMarkdownToHtmlConverter(private val project: Project) {
.replace("&apos;", "'")
.replace("&amp;", "&")
}
companion object {
private val HTML_BASE_TAGS_REGEX = Regex("<(/)?(html|head|body)( [^>]*)?>")
private val PRE_TAG_REGEX = Regex("<pre>")
private val P_TAG_REGEX = Regex("<p>")
private val BLOCKQUOTE_REGEX = Regex("<blockquote>(.*?)</blockquote>", RegexOption.DOT_MATCHES_ALL)
private val TABLE_REGEX = Regex("<table.*?>(.*?)</table>", RegexOption.DOT_MATCHES_ALL)
private val TD_REGEX = Regex("<td>(.*?)</td>", RegexOption.DOT_MATCHES_ALL)
}
}
@@ -2,6 +2,7 @@ package com.intellij.python.community.impl.huggingFace.documentation
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.util.text.HtmlChunk
import com.intellij.python.community.impl.huggingFace.service.PyHuggingFaceBundle
import com.intellij.ui.scale.JBUIScale
import org.intellij.lang.annotations.Language
@@ -28,8 +29,7 @@ object HuggingFaceQuickDocStyles { // see PY-70541
const val HAIR_SPACE = "&ensp;"
@Language("CSS")
@NlsSafe
private val styleContent = listOf(
val styleRules = listOf(
"$HF_P_TAG_CLASS { margin-top: ${scale(4)}px; margin-bottom: ${scale(6)}px; }",
".$CODE_DIV_CLASS { padding-top: ${scale(4)}px; padding-bottom: ${scale(4)}px; padding-left: ${scale(4)}px; " +
@@ -47,7 +47,10 @@ object HuggingFaceQuickDocStyles { // see PY-70541
"blockquote { border-left: ${scale(4)}px solid #cccccc; }",
"blockquote p { border-left: none; }",
).joinToString(separator = " ")
)
fun styleChunk(): HtmlChunk = HtmlChunk.raw("<style>$styleContent</style>")
@NlsSafe
private val styleContent = styleRules.joinToString(separator = " ")
fun styleChunk(): HtmlChunk = HtmlChunk.raw(PyHuggingFaceBundle.message("python.hugging.face.tags.style", styleContent))
}
@@ -1,12 +1,12 @@
package com.intellij.python.community.impl.huggingFace.documentation
import com.intellij.python.community.impl.huggingFace.HuggingFaceConstants
import com.intellij.python.community.impl.huggingFace.HuggingFaceEntityKind
import com.intellij.python.community.impl.huggingFace.api.HuggingFaceURLProvider
import org.jetbrains.annotations.ApiStatus
import java.net.URL
import java.util.*
@ApiStatus.Internal
class HuggingFaceReadmeCleaner(
private var markdown: String,
private val entityId: String,
@@ -180,36 +180,6 @@ class HuggingFaceReadmeCleaner(
}
}
@Suppress("unused") // may be activated if we decide to trim model cards
private fun trimLongMd() {
markdown = if (markdown.length < HuggingFaceConstants.MAX_MD_CHAR_NUM) markdown else {
var trimmedMd = markdown.substring(0, HuggingFaceConstants.MAX_MD_CHAR_NUM)
val numCodeFences = trimmedMd.split(CODE_FENCE_MARKER).size - 1
// Check for an odd number of code fences
if (numCodeFences % 2 != 0) {
val lastCodeFenceIndex = trimmedMd.lastIndexOf("```")
if (lastCodeFenceIndex != -1) {
trimmedMd = trimmedMd.substring(0, lastCodeFenceIndex)
}
}
val sentenceBoundary = trimmedMd.lastIndexOf(". ")
val imageBoundary = trimmedMd.lastIndexOf("![")
val tableBoundary = trimmedMd.lastIndexOf("|")
val lastNewLine = trimmedMd.lastIndexOf("\n")
val maxBoundary = maxOf(sentenceBoundary, imageBoundary, tableBoundary, lastNewLine)
if (maxBoundary != -1) {
trimmedMd = trimmedMd.substring(0, maxBoundary + 1)
}
val placeholder = HuggingFaceDocumentationPlaceholdersUtil.trimmedMdPlaceholder(entityId, entityKind)
"${trimmedMd.trimEnd()}\n\n${placeholder}"
}
}
companion object {
private const val HF_MD_HEADER_SEPARATOR = "---\n"
private const val ERR_PY_CODE_FENCE_HEADER = "```py\n"
@@ -1,20 +0,0 @@
// 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.python.community.impl.huggingFace.icons;
import com.intellij.ui.IconManager;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
/**
* NOTE THIS FILE IS AUTO-GENERATED
* DO NOT EDIT IT BY HAND, run "Generate icon classes" configuration instead
*/
public final class PythonCommunityImplHuggingFaceIcons {
private static @NotNull Icon load(@NotNull String path, int cacheKey, int flags) {
return IconManager.getInstance().loadRasterizedIcon(path, PythonCommunityImplHuggingFaceIcons.class.getClassLoader(), cacheKey, flags);
}
/** 14x14 */ public static final @NotNull Icon Download = load("com/intellij/python/community/impl/huggingFace/icons/download.svg", -459821553, 0);
/** 14x14 */ public static final @NotNull Icon Like = load("com/intellij/python/community/impl/huggingFace/icons/like.svg", -1702501286, 0);
/** 16x16 */ public static final @NotNull Icon Logo = load("com/intellij/python/community/impl/huggingFace/icons/logo.svg", 59143455, 0);
}
@@ -7,12 +7,14 @@ import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.Logger
import com.intellij.platform.util.coroutines.childScope
import kotlinx.coroutines.*
import org.jetbrains.annotations.ApiStatus
import kotlin.time.Duration
@ApiStatus.Internal
@Service(Service.Level.APP)
class HuggingFaceExecutorService(val coroutineScope: CoroutineScope)
@ApiStatus.Internal
@Service(Service.Level.APP)
class HuggingFaceSafeExecutor(private val coroutineScope: CoroutineScope, private val defaultTimeout: Duration): Disposable {
@@ -5,39 +5,26 @@ import com.intellij.openapi.components.Service
import com.intellij.openapi.editor.EditorFactory
import com.intellij.openapi.editor.event.DocumentEvent
import com.intellij.openapi.editor.event.DocumentListener
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.openapi.vfs.newvfs.BulkFileListener
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
import com.intellij.openapi.project.Project
import com.intellij.python.community.impl.huggingFace.HuggingFaceUtil
import com.intellij.util.messages.MessageBusConnection
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
@Service(Service.Level.PROJECT)
class HuggingFaceImportedLibrariesManagerService(project: Project) {
private val manager = HuggingFaceImportedLibrariesManager(project)
fun getManager(): HuggingFaceImportedLibrariesManager {
return manager
}
}
class HuggingFaceImportedLibrariesManager(project: Project) {
class HuggingFaceImportedLibrariesManager(val project: Project) {
private var cachedResult: Result? = null
private var cacheTimestamp: Long = 0
private val invalidationThresholdMinutes = 5
private var changesCount: Int = 0
private val changesThreshold = 10
private var project: Project? = null
private data class Result(val isImported: Boolean)
init {
this.project = project
val connection: MessageBusConnection = project.messageBus.connect()
connection.subscribe(VirtualFileManager.VFS_CHANGES, object : BulkFileListener {
override fun after(events: List<VFileEvent>) {
@@ -60,7 +47,7 @@ class HuggingFaceImportedLibrariesManager(project: Project) {
}
fun isLibraryImported(): Boolean {
val project = this.project ?: throw IllegalStateException("Init the ImportedLibrariesManager with project first.")
val project = this.project
if (System.currentTimeMillis() - cacheTimestamp > invalidationThresholdMinutes * 60 * 1000 || cachedResult == null) {
val isImported = HuggingFaceUtil.isAnyHFLibraryImportedInProject(project)
if (isImported) {
@@ -5,7 +5,7 @@ import com.intellij.codeInspection.InspectionSuppressor
import com.intellij.codeInspection.SuppressQuickFix
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFileSystemItem
import com.intellij.python.community.impl.huggingFace.annotation.HuggingFaceEntityPsiElement.Companion.HUGGING_FACE_ENTITY_NAME_KEY
import com.intellij.python.community.impl.huggingFace.annotation.HuggingFaceIdentifierPsiElement.Companion.HUGGING_FACE_ENTITY_NAME_KEY
import com.jetbrains.python.psi.PyFile
import com.jetbrains.python.psi.impl.PyPlainStringElementImpl