-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(intellij): add support to collect declaration snippets. (#3394)
* feat(intellij): add support to collect declaration snippets. * fix(intellij): add try-catch for findTargetElement.
- Loading branch information
Showing
10 changed files
with
385 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
88 changes: 88 additions & 0 deletions
88
...c/main/kotlin/com/tabbyml/intellijtabby/languageSupport/DefaultLanguageSupportProvider.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
package com.tabbyml.intellijtabby.languageSupport | ||
|
||
import com.intellij.codeInsight.TargetElementUtil | ||
import com.intellij.openapi.application.runReadAction | ||
import com.intellij.openapi.diagnostic.logger | ||
import com.intellij.openapi.project.Project | ||
import com.intellij.psi.PsiElement | ||
import com.intellij.psi.PsiRecursiveElementWalkingVisitor | ||
import com.tabbyml.intellijtabby.findEditor | ||
import com.tabbyml.intellijtabby.languageSupport.LanguageSupportProvider.* | ||
import org.eclipse.lsp4j.SemanticTokenTypes | ||
|
||
/** | ||
* The default implementation of [LanguageSupportProvider]. | ||
* This implementation relies on [TargetElementUtil] and tries to find the navigation target at each position in the | ||
* editor to provide semantic tokens and declarations. | ||
* This implementation may not work effectively for all languages. | ||
*/ | ||
open class DefaultLanguageSupportProvider : LanguageSupportProvider { | ||
private val logger = logger<DefaultLanguageSupportProvider>() | ||
private val targetElementUtil = TargetElementUtil.getInstance() | ||
|
||
override fun provideSemanticTokensRange(project: Project, fileRange: FileRange): List<SemanticToken>? { | ||
val psiFile = fileRange.file | ||
val editor = project.findEditor(psiFile.virtualFile) ?: return null | ||
|
||
return runReadAction { | ||
val leafElements = mutableListOf<PsiElement>() | ||
psiFile.accept(object : PsiRecursiveElementWalkingVisitor() { | ||
override fun visitElement(element: PsiElement) { | ||
if (element.children.isEmpty() && | ||
element.text.matches(Regex("\\w+")) && | ||
fileRange.range.contains(element.textRange) && | ||
leafElements.none { it.textRange.intersects(element.textRange) } | ||
) { | ||
leafElements.add(element) | ||
} | ||
if (element.textRange.intersects(fileRange.range)) { | ||
super.visitElement(element) | ||
} | ||
} | ||
}) | ||
|
||
leafElements.mapNotNull { | ||
val target = try { | ||
targetElementUtil.findTargetElement( | ||
editor.editor, | ||
TargetElementUtil.ELEMENT_NAME_ACCEPTED or TargetElementUtil.REFERENCED_ELEMENT_ACCEPTED, | ||
it.textRange.startOffset | ||
) | ||
} catch (e: Exception) { | ||
logger.debug("Failed to find target element when providing semantic tokens", e) | ||
null | ||
} | ||
if (target == it || target == null || target.text == null) { | ||
null | ||
} else { | ||
SemanticToken( | ||
text = it.text, | ||
range = it.textRange, | ||
type = SemanticTokenTypes.Type, // Default to use `Type` as the token type as we don't know the actual type | ||
) | ||
} | ||
} | ||
} | ||
} | ||
|
||
override fun provideDeclaration(project: Project, filePosition: FilePosition): List<FileRange>? { | ||
val psiFile = filePosition.file | ||
val editor = project.findEditor(psiFile.virtualFile) ?: return null | ||
|
||
return runReadAction { | ||
val target = try { | ||
targetElementUtil.findTargetElement( | ||
editor.editor, | ||
TargetElementUtil.ELEMENT_NAME_ACCEPTED or TargetElementUtil.REFERENCED_ELEMENT_ACCEPTED, | ||
filePosition.offset | ||
) | ||
} catch (e: Exception) { | ||
logger.debug("Failed to find target element at ${psiFile.virtualFile.url}:${filePosition.offset}", e) | ||
null | ||
} | ||
val file = target?.containingFile ?: return@runReadAction listOf() | ||
val range = target.textRange | ||
listOf(FileRange(file, range)) | ||
} | ||
} | ||
} |
51 changes: 51 additions & 0 deletions
51
...llij/src/main/kotlin/com/tabbyml/intellijtabby/languageSupport/LanguageSupportProvider.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
package com.tabbyml.intellijtabby.languageSupport | ||
|
||
import com.intellij.openapi.project.Project | ||
import com.intellij.openapi.util.TextRange | ||
import com.intellij.psi.PsiFile | ||
|
||
interface LanguageSupportProvider { | ||
data class FilePosition( | ||
val file: PsiFile, | ||
val offset: Int, | ||
) | ||
|
||
data class FileRange( | ||
val file: PsiFile, | ||
val range: TextRange, | ||
) | ||
|
||
data class SemanticToken( | ||
val text: String, | ||
val range: TextRange, | ||
/** | ||
* See [org.eclipse.lsp4j.SemanticTokenTypes] | ||
*/ | ||
val type: String, | ||
/** | ||
* See [org.eclipse.lsp4j.SemanticTokenModifiers] | ||
*/ | ||
val modifiers: List<String> = emptyList(), | ||
) | ||
|
||
/** | ||
* Find all semantic tokens in the given [fileRange]. | ||
* For now, this function is only used to find tokens that reference a declaration, which will be used to invoke [provideDeclaration] later. | ||
* So it is safe to only contain these tokens in the result, like class names, function names, etc. | ||
* | ||
* If no tokens are found, return an empty list. | ||
* If the provider does not support the given document, return null. | ||
*/ | ||
fun provideSemanticTokensRange(project: Project, fileRange: FileRange): List<SemanticToken>? { | ||
return null | ||
} | ||
|
||
/** | ||
* Get the declaration location for the token at the given [filePosition]. | ||
* If no declaration is found, return an empty list. | ||
* If the provider does not support the given document, return null. | ||
*/ | ||
fun provideDeclaration(project: Project, filePosition: FilePosition): List<FileRange>? { | ||
return null | ||
} | ||
} |
48 changes: 48 additions & 0 deletions
48
...ellij/src/main/kotlin/com/tabbyml/intellijtabby/languageSupport/LanguageSupportService.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package com.tabbyml.intellijtabby.languageSupport | ||
|
||
import com.intellij.openapi.components.Service | ||
import com.intellij.openapi.diagnostic.logger | ||
import com.intellij.openapi.extensions.ExtensionPointName | ||
import com.intellij.openapi.project.Project | ||
import com.tabbyml.intellijtabby.languageSupport.LanguageSupportProvider.* | ||
|
||
|
||
@Service(Service.Level.PROJECT) | ||
class LanguageSupportService(private val project: Project) { | ||
private val logger = logger<LanguageSupportService>() | ||
private val languageSupportProviderExtensionPoint: ExtensionPointName<LanguageSupportProvider> = | ||
ExtensionPointName.create("com.tabbyml.intellij-tabby.languageSupportProvider") | ||
private val defaultLanguageSupportProvider = DefaultLanguageSupportProvider() | ||
|
||
fun provideSemanticTokensRange(fileRange: FileRange): List<SemanticToken>? { | ||
var semanticTokens: List<SemanticToken>? = null | ||
for (provider in languageSupportProviderExtensionPoint.extensionList) { | ||
semanticTokens = provider.provideSemanticTokensRange(project, fileRange) | ||
if (semanticTokens != null) { | ||
logger.trace("Semantic tokens provided by ${provider.javaClass.name}: $semanticTokens") | ||
break | ||
} | ||
} | ||
if (semanticTokens == null) { | ||
semanticTokens = defaultLanguageSupportProvider.provideSemanticTokensRange(project, fileRange) | ||
logger.trace("Semantic tokens provided by default provider: $semanticTokens") | ||
} | ||
return semanticTokens | ||
} | ||
|
||
fun provideDeclaration(position: FilePosition): List<FileRange>? { | ||
var declaration: List<FileRange>? = null | ||
for (provider in languageSupportProviderExtensionPoint.extensionList) { | ||
declaration = provider.provideDeclaration(project, position) | ||
if (declaration != null) { | ||
logger.trace("Declaration provided by ${provider.javaClass.name}: $declaration") | ||
break | ||
} | ||
} | ||
if (declaration == null) { | ||
declaration = defaultLanguageSupportProvider.provideDeclaration(project, position) | ||
logger.trace("Declaration provided by default provider: $declaration") | ||
} | ||
return declaration | ||
} | ||
} |
Oops, something went wrong.