Skip to content

Commit

Permalink
Richer search results via LookupElement
Browse files Browse the repository at this point in the history
  • Loading branch information
garyttierney committed Apr 13, 2024
1 parent 59410c7 commit 7128521
Show file tree
Hide file tree
Showing 9 changed files with 158 additions and 48 deletions.
3 changes: 2 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ dependencies {

implementation(libs.jewel.standalone)
implementation(libs.jewel.decorated.window)

implementation(libs.fastutil)
implementation(libs.jetbrains.compose.splitpane)
implementation(compose.desktop.currentOs) {
exclude(group = "org.jetbrains.compose.material")
}

implementation(libs.intellij.util.text.matching)
implementation(libs.intellij.util.base)
implementation(libs.intellij.util.ui)
implementation(libs.intellij.icons)

implementation(fileTree(ghidraDistribution) {
Expand Down
20 changes: 20 additions & 0 deletions src/main/kotlin/framework/LookupElement.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.github.garyttierney.ghidralite.framework

import androidx.compose.runtime.Composable

interface LookupElement {
val key: Any
val label: String
val parent: LookupElement?
val icon: @Composable () -> Unit

fun ancestors(): Sequence<LookupElement> = sequence {
val p = parent
if (p != null) {
yield(p)
yieldAll(p.ancestors())
}
}

fun fullyQualified() = label
}
40 changes: 39 additions & 1 deletion src/main/kotlin/framework/db/SymbolDatabase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,44 @@ package io.github.garyttierney.ghidralite.framework.db
import GhidraField
import GhidraSchema
import GhidraType
import androidx.compose.runtime.Composable
import com.intellij.icons.ExpUiIcons
import db.DBRecord
import db.Table
import ghidra.program.model.symbol.SymbolType
import io.github.garyttierney.ghidralite.framework.LookupElement
import org.jetbrains.jewel.ui.component.*

data class SymbolLookupDetails(
val id: Long,
val type: SymbolType,
override val label: String,
override val parent: SymbolLookupDetails?
) : LookupElement {
override val key: Any = id
override val icon = @Composable {
val iconPath = when (type) {
SymbolType.FUNCTION -> "/expui/nodes/function_dark.svg"
SymbolType.NAMESPACE -> "/expui/nodes/package_dark.svg"
SymbolType.GLOBAL_VAR -> "/expui/nodes/gvariable_dark.svg"
SymbolType.LIBRARY -> "/expui/nodes/library_dark.svg"
SymbolType.CLASS -> "/expui/nodes/class_dark.svg"
else -> "/expui/nodes/field_dark.svg"
}

Icon(
resource = iconPath,
iconClass = ExpUiIcons::class.java,
contentDescription = type.toString()
)
}

override fun fullyQualified(): String {
return ancestors().fold("") { value, lookupElement ->
"${lookupElement.label}::$value"
}
}
}


@GhidraSchema(
Expand All @@ -24,7 +59,10 @@ interface SymbolRecord : GhidraRecord {
var typeOrdinal: Byte
var type: SymbolType
get() = SymbolType.getSymbolType(typeOrdinal.toInt())
set(value) { typeOrdinal = value.id }
set(value) {
typeOrdinal = value.id
}

}

class SymbolDbTable(inner: Table) : GhidraTable<SymbolRecord>(inner) {
Expand Down
2 changes: 2 additions & 0 deletions src/main/kotlin/framework/db/db.kt
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ class GhidraRecordSpliterator<T : GhidraRecord>(
abstract class GhidraTable<T : GhidraRecord>(val inner: Table) {
internal abstract fun from(record: DBRecord): T

fun get(id: Long) = from(inner.getRecord(id))

fun all() = StreamSupport.stream(
GhidraRecordSpliterator(
this,
Expand Down
34 changes: 20 additions & 14 deletions src/main/kotlin/framework/search/Searcher.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,41 @@ package io.github.garyttierney.ghidralite.framework.search

import com.intellij.openapi.util.TextRange
import com.intellij.psi.codeStyle.NameUtil
import io.github.garyttierney.ghidralite.framework.db.SymbolRecord
import io.github.garyttierney.ghidralite.framework.LookupElement
import io.github.garyttierney.ghidralite.framework.db.SymbolLookupDetails
import io.github.garyttierney.ghidralite.framework.search.index.Indexes
import java.util.concurrent.PriorityBlockingQueue
import java.util.concurrent.atomic.AtomicInteger

public class Searcher(val indexes: Indexes) {
public suspend fun query(query: String, onDataAvailable: (List<SearchResult>) -> Unit) {
class Searcher(private val indexes: Indexes) {
suspend fun query(query: String, onDataAvailable: (List<SearchResult>) -> Unit) {
val priorityQueue = PriorityBlockingQueue<SearchResult>()
val emissions = AtomicInteger(0)

val queueCapacity = 50
val matcher = NameUtil.buildMatcher(query).withCaseSensitivity(NameUtil.MatchingCaseSensitivity.NONE)
.withSeparators(".:_").typoTolerant().allOccurrences().build()

indexes.query<SymbolRecord>().collect {
val matchingDegree = matcher.matchingDegree(it.name, false)
val matcher = NameUtil.buildMatcher(query)
.withCaseSensitivity(NameUtil.MatchingCaseSensitivity.NONE)
.withSeparators(".:_")
.typoTolerant()
.allOccurrences()
.build()

indexes.query<SymbolLookupDetails>().collect {
val matchingDegree = matcher.matchingDegree(it.label, false)
val result = when {
matchingDegree < 1 -> return@collect
else -> SearchResult(it.name, it.name, it.name, matchingDegree, it.key, mutableListOf())
else -> SearchResult(
it,
matchingDegree,
mutableListOf()
)
}

val beatsWorst = priorityQueue.size == queueCapacity && priorityQueue.peek().score < result.score
val adding = beatsWorst || priorityQueue.size < queueCapacity

if (adding) {
val fragments = matcher.matchingFragments(it.name)
val fragments = matcher.matchingFragments(it.label)
result.fragments.addAll(fragments)
priorityQueue.add(result)

Expand All @@ -43,11 +52,8 @@ public class Searcher(val indexes: Indexes) {
}

data class SearchResult(
val name: String,
val subheading: String,
val type: String,
val element: LookupElement,
val score: Int,
val uniqueKey: Long,
val fragments: MutableList<TextRange>
) : Comparable<SearchResult> {
override fun compareTo(other: SearchResult): Int = score.compareTo(other.score)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.github.garyttierney.ghidralite.framework.search.index.program.symbol

import ghidra.program.model.symbol.SymbolType
import io.github.garyttierney.ghidralite.framework.db.SymbolDbTable
import io.github.garyttierney.ghidralite.framework.db.SymbolLookupDetails
import io.github.garyttierney.ghidralite.framework.db.SymbolRecord
import io.github.garyttierney.ghidralite.framework.search.index.IndexBulkLoader
import it.unimi.dsi.fastutil.longs.Long2ObjectArrayMap
import it.unimi.dsi.fastutil.longs.Long2ObjectFunction
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.stream.consumeAsFlow

class SymbolIndexLoader(private val table: SymbolDbTable) : IndexBulkLoader<SymbolLookupDetails> {
override suspend fun load(): Flow<SymbolLookupDetails> {
val namespaceCache = Long2ObjectArrayMap<SymbolLookupDetails>()

fun recordToLookup(record: SymbolRecord): SymbolLookupDetails {
val cacheLoader = Long2ObjectFunction { key -> recordToLookup(table.get(key)) }

return SymbolLookupDetails(
record.key,
record.type,
record.name,
if (record.parentId == 0L) null else namespaceCache.computeIfAbsent(record.parentId, cacheLoader)
)
}

return table.all().consumeAsFlow()
.filterNot { it.type == SymbolType.LABEL || it.type == SymbolType.NAMESPACE }
.map(::recordToLookup)
}
}
9 changes: 3 additions & 6 deletions src/main/kotlin/ui/root/Workspace.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import io.github.garyttierney.ghidralite.framework.search.index.IndexWriter
import io.github.garyttierney.ghidralite.framework.search.index.Indexes
import io.github.garyttierney.ghidralite.framework.search.index.program.ProgramChangeSnapshotStrategy
import io.github.garyttierney.ghidralite.framework.search.index.program.ProgramChangeWatcher
import io.github.garyttierney.ghidralite.framework.search.index.program.ProgramDbTableLoader
import io.github.garyttierney.ghidralite.framework.search.index.program.programChangeInterest
import io.github.garyttierney.ghidralite.framework.search.index.program.symbol.SymbolIndexLoader
import kotlinx.coroutines.launch

object SymbolSnapshotStrategy : ProgramChangeSnapshotStrategy<SymbolRecord> {
Expand Down Expand Up @@ -56,13 +56,10 @@ class Workspace(val project: Project, val program: Program, val searcher: Search
)
)
val symbolDbTable = SymbolDbTable(program.dbHandle.getTable("Symbols"))
val regex = Regex("^case|RTTI|Catch@|Unwind@|switch")
val symbolLoader = ProgramDbTableLoader(symbolDbTable) {
!it.name.matches(regex)
}
val symbolIndexLoader = SymbolIndexLoader(symbolDbTable)
val symbolIndexWriter = IndexWriter(indexes, SymbolRecord::class)

indexes.load(symbolLoader)
indexes.load(symbolIndexLoader)

GhidraWorkerScope.launch {
symbolIndexWriter.run(symbolChanges)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package io.github.garyttierney.ghidralite.ui.search

import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.background
import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.focusable
Expand All @@ -9,19 +8,20 @@ import androidx.compose.foundation.rememberScrollbarAdapter
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import io.github.garyttierney.ghidralite.framework.search.SearchResult
import io.github.garyttierney.ghidralite.ui.internal.PreviewComponent
import kotlinx.coroutines.launch
import org.jetbrains.jewel.foundation.lazy.*
import org.jetbrains.jewel.foundation.lazy.tree.DefaultSelectableLazyColumnKeyActions
import org.jetbrains.jewel.foundation.lazy.tree.KeyActions
Expand All @@ -31,7 +31,6 @@ import org.jetbrains.jewel.ui.component.*
import org.jetbrains.jewel.ui.theme.colorPalette
import org.jetbrains.jewel.ui.theme.menuStyle

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun QuickSearch(
items: List<SearchResult>,
Expand All @@ -42,7 +41,7 @@ fun QuickSearch(
listActions: KeyActions = remember { DefaultSelectableLazyColumnKeyActions(DefaultSelectableColumnKeybindings) }
) {
val itemListState = rememberSelectableLazyListState()
val itemKeys by derivedStateOf { items.map { SelectableLazyListKey.Selectable(it.uniqueKey) }.toList() }
val itemKeys by derivedStateOf { items.map { SelectableLazyListKey.Selectable(it.element.key) }.toList() }

val handleListAction = { event: KeyEvent ->
val itemListHandler = listActions.handleOnKeyEvent(event, itemKeys, itemListState, SelectionMode.Single)
Expand Down Expand Up @@ -87,16 +86,25 @@ fun QuickSearchResultList(
state: SelectableLazyListState = rememberSelectableLazyListState(),
) {
val listTheme = Modifier.fillMaxSize().background(JewelTheme.menuStyle.colors.background)
val scope = rememberCoroutineScope()

Box {
SelectableLazyColumn(
contentPadding = PaddingValues(2.dp),
selectionMode = SelectionMode.Single,
state = state,
modifier = listTheme.focusable(),
modifier = listTheme.then(Modifier.focusable()),
onSelectedIndexesChanged = {
val first = it.firstOrNull()
first?.let {
scope.launch {
state.scrollToItem(it)
}
}
}
) {
items.forEach {
item(key = it.uniqueKey) {
item(key = it.element.key) {
QuickSearchResult(item = it)
}
}
Expand Down Expand Up @@ -130,12 +138,7 @@ fun SelectableLazyItemScope.QuickSearchResult(item: SearchResult) {
verticalAlignment = Alignment.Bottom,
modifier = rowTheme
) {
Icon(
"/nodes/method.svg",
contentDescription = "Test",
iconClass = SearchResult::class.java,
modifier = Modifier.size(16.dp)
)
item.element.icon()

val matchingRangeAnnotations = item.fragments.map { range ->
AnnotatedString.Range(
Expand All @@ -145,23 +148,22 @@ fun SelectableLazyItemScope.QuickSearchResult(item: SearchResult) {
)
}

val annotatedLabel = buildAnnotatedString {
append(AnnotatedString(text = item.element.label, spanStyles = matchingRangeAnnotations))
val parent = item.element.parent

if (parent != null) {
append(" ")
pushStyle(SpanStyle(color = JewelTheme.globalColors.infoContent, fontWeight = FontWeight.Light))
append(parent.fullyQualified())
}
}

Text(
AnnotatedString(text = item.name, spanStyles = matchingRangeAnnotations),
annotatedLabel,
maxLines = 1,
modifier = Modifier.weight(1f, false),
overflow = TextOverflow.Ellipsis
)
}
}

@Preview
@Composable
internal fun QuickSearchPreview() = PreviewComponent {
val items = (0..10).map {
SearchResult(
"name", "subheading", "type", 1, it as Long, mutableListOf()
)
}

QuickSearch(items = items.toList(), query = "Search query", onQueryChanged = {}, onResultSelected = {})
}
10 changes: 10 additions & 0 deletions versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ detekt = "1.23.4"
dokka = "1.8.20"
ideaGradlePlugin = "1.17.1"
intellij = "241.14494.241"
fastutil = "8.5.13"
ghidra = "11.1"
jewel = "0.17.0"
jna = "5.14.0"
Expand All @@ -15,6 +16,7 @@ kotlinpoet = "1.15.2"
kotlinterGradlePlugin = "3.16.0"
kotlinxSerialization = "1.5.1"
kotlinxBinaryCompat = "0.14.0"
lucene = "9.10.0"
ksp = "1.9.20-1.0.14"
poko = "0.13.1"

Expand All @@ -25,12 +27,20 @@ commonmark-ext-autolink = { module = "org.commonmark:commonmark-ext-autolink", v

decompose = { module = "com.arkivanov.decompose:decompose", version.ref = "decompose" }
decompose-extensions-jetbrains = { module = "com.arkivanov.decompose:extensions-compose-jetbrains", version.ref = "decompose" }

fastutil = { module = "it.unimi.dsi:fastutil", version.ref = "fastutil" }

filePicker = { module = "com.darkrockstudios:mpfilepicker", version = "3.1.0" }

intellij-icons = { module = "com.jetbrains.intellij.platform:icons", version.ref = "intellij" }
intellij-util-text-matching = { module = "com.jetbrains.intellij.platform:util-text-matching", version.ref = "intellij" }
intellij-util-base = { module = "com.jetbrains.intellij.platform:util-base", version.ref = "intellij" }
intellij-util-ui = { module = "com.jetbrains.intellij.platform:util-ui", version.ref = "intellij" }

jetbrains-compose-splitpane = { module = "org.jetbrains.compose.components:components-splitpane-desktop", version.ref = "composeDesktop" }

lucene-core = { module = "org.apache.lucene:lucene-core", version.ref = "lucene" }

kotlinSarif = { module = "io.github.detekt.sarif4k:sarif4k", version.ref = "kotlinSarif" }
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
Expand Down

0 comments on commit 7128521

Please sign in to comment.