plugin graph viewer - tooltips

GitOrigin-RevId: 02f26829ad706f1418a1463afac1866e062b9fe1
This commit is contained in:
Vladimir Krivosheev
2021-06-03 11:00:37 +02:00
committed by intellij-monorepo-bot
parent f99d081f68
commit 5abfa8be7e
14 changed files with 810 additions and 357 deletions

3
docs/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
out/plugin-graph
plugin-graph/node_modules
.DS_Store

View File

@@ -1,68 +0,0 @@
<html lang="en">
<head>
<meta charset="utf-8">
<title>Plugin Graph</title>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
<link rel="preconnect" href="https://cdn.jsdelivr.net">
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/cytoscape@3.19.0/dist/cytoscape.min.js"
integrity="sha256-Q5aff3EY3fW5pgDK43AnI19LbbryZdLXCmsipdPDwWM=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/flexsearch@0.6.32/dist/flexsearch.min.js"
integrity="sha256-RbOWIPK8D/wzx66aEmf1i32S511+EQGFKxtXoepYKhA=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/layout-base@2.0.0/layout-base.js"
integrity="sha256-z6wEGjx+aPziAQlCx29FoQUoSy+k/VCVUzNSZJAncP8=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/cose-base@2.0.0/cose-base.js" integrity="sha256-l5cuNpyJp9j3JcIsngoR2+prUPZSg8FbXihfXCrwSDc="
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/cytoscape-fcose@2.0.0/cytoscape-fcose.js"
integrity="sha256-J/3ZxWPNEF36ZZz7xnSZCC/QkH8VfgjQBNtktLRXjGU=" crossorigin="anonymous"></script>
<script src="plugin-graph.js" type="text/javascript"></script>
<!--suppress CssUnusedSymbol -->
<style>
.tooltipMainValue {
float: right;
margin-left: 20px;
font-weight: 900;
}
.tooltipValue {
float: right;
margin-left: 20px;
}
.tooltipSelectableValue {
float: right;
margin-left: 20px;
user-select: text
}
.abcC {
position: absolute;
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
text-align: center;
z-index: 100;
}
</style>
</head>
<body>
<div class="abcC">
<input id="searchField" type="search" placeholder="filter"
spellcheck="false"
autofocus/>
</div>
<div id="cy" style="width:100%; height:100%;">
</div>
</body>
</html>

View File

@@ -1,289 +0,0 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
fetch("plugin-graph.json")
.then(it => it.json())
.then(graph => {
const listener = () => initGraph(graph)
if (document.readyState === "loading") {
window.addEventListener("DOMContentLoaded", listener)
}
else {
listener()
}
})
function getItemSizeStyle(factor) {
const baseNodeDiameter = 10
const size = `${Math.ceil(baseNodeDiameter * factor)}px`
return {
"width": size,
"height": size,
}
}
function initGraph(graph) {
// noinspection SpellCheckingInspection
const layoutOptions = {
name: "fcose",
quality: "proof",
randomize: false,
nodeDimensionsIncludeLabels: true,
}
// noinspection SpellCheckingInspection
const cy = cytoscape({
container: document.getElementById("cy"),
elements: graph,
layout: layoutOptions,
minZoom: 0.4,
maxZoom: 3,
autoungrabify: true,
style: [
{
selector: "node",
style: {
"label": (e) => {
const name = e.data("name")
if (name.startsWith("intellij.")) {
return `i.${name.substring("intellij.".length)}`
}
else if (name.startsWith("com.intellij.modules.")) {
return `c.i.m.${name.substring("com.intellij.modules.".length)}`
}
else {
return name
}
},
"font-family": "JetBrains Mono",
"font-size": 13,
"color": "#515151",
},
},
{
selector: "node[type=0]",
style: getItemSizeStyle(1),
},
{
selector: "node[type=1]",
style: getItemSizeStyle(1.2),
},
{
selector: "node[type=2]",
style: getItemSizeStyle(1.4),
},
{
selector: "edge",
style: {
"curve-style": "straight",
"width": 1,
}
},
{
selector: "edge[type=0]",
style: {
"target-arrow-shape": "triangle-backcurve",
"arrow-scale": 0.8,
},
},
{
selector: "edge[type=1]",
style: {
"target-arrow-shape": "square",
// square is too big
"arrow-scale": 0.5,
},
},
// highlighting (https://stackoverflow.com/a/38468892)
{
selector: "node.semiTransparent",
style: {"opacity": "0.5"}
},
{
selector: "edge.highlight",
style: {"mid-target-arrow-color": "#FFF"}
},
{
selector: "edge.semiTransparent",
style: {"opacity": "0.2"}
},
{
selector: "node.found",
style: {
"font-weight": "600",
},
},
]
})
// ensure that dragging of element causes panning and not selecting
// https://github.com/cytoscape/cytoscape.js/issues/1905
cy.elements().panify()
function debounce(func) {
let timeout
return function (...args) {
clearTimeout(timeout)
const handler = function() {
func.apply(null, args)
}
timeout = setTimeout(handler, 300)
}
}
const search = new GraphTextSearch(graph, cy)
document.getElementById("searchField").addEventListener("input", debounce(function (event) {
search.searchNodes(event.target.value.trim())
}))
new GraphHighlighter(cy, search)
}
function buildTooltip(lines) {
let result = ""
for (const line of lines) {
if (line.main) {
result += `<span style="user-select: text">${line.name}</span>`
}
else {
result += `<br/>${line.name}`
}
const valueStyleClass = line.selectable ? "tooltipSelectableValue" : (line.main ? "tooltipMainValue" : "tooltipValue")
if (line.value != null) {
result += `<span class="${valueStyleClass}"`
if (line.extraStyle != null && line.extraStyle.length > 0) {
result += ` style="${line.extraStyle}"`
}
if (line.hint != null && line.hint.length !== 0) {
result += ` title="${line.hint}"`
}
result += `>${line.value}</span>`
}
}
return result
}
class GraphHighlighter {
constructor(cy, graphTextSearch) {
this.cy = cy
this.graphTextSearch = graphTextSearch
cy.on("mouseover", "node", e => {
this.selectNode(e.target)
})
cy.on("mouseout", "node", e => {
this.deselectNode(e.target)
})
}
selectNode(selection) {
const cy = this.cy
if (!this.graphTextSearch.totalUnion.empty()) {
cy.elements().difference(this.graphTextSearch.totalUnion).removeClass("semiTransparent")
}
const toHighlight = selection.outgoers().union(selection.incomers()).union(selection)
cy.elements().difference(toHighlight).difference(this.graphTextSearch.totalUnion).addClass("semiTransparent")
toHighlight.difference(this.graphTextSearch.totalUnion).addClass("highlight")
}
deselectNode(selection) {
const cy = this.cy
cy.elements().removeClass("semiTransparent")
if (!this.graphTextSearch.totalUnion.empty()) {
cy.elements().difference(this.graphTextSearch.totalUnion).addClass("semiTransparent")
}
selection.outgoers().union(selection.incomers()).union(selection).difference(this.graphTextSearch.totalUnion).removeClass("highlight")
}
}
class GraphTextSearch {
constructor(graph, cy) {
this.cy = cy
this.selectedNodes = new Set()
this.totalUnion = cy.collection()
this.index = new FlexSearch({
tokenize: "strict",
depth: 3,
doc: {
id: "data:id",
field: [
"data:name",
"data:pluginId",
"data:sourceModule",
"data:package",
]
}
})
this.index.add(graph)
}
searchNodes(text) {
const {selectedNodes, cy, index} = this
const newNodes = []
if (text.length !== 0) {
for (const item of index.search(text)) {
const node = cy.getElementById(item.data.id)
if (node == null) {
console.error(`Cannot find node by id ${item.data.id}`)
}
selectedNodes.delete(node)
newNodes.push(node)
}
}
cy.elements().removeClass("semiTransparent")
for (const prevNode of selectedNodes) {
prevNode.removeClass(["highlight", "found"])
prevNode.outgoers().union(prevNode.incomers()).removeClass("highlight")
}
selectedNodes.clear()
if (newNodes.length === 0) {
this.totalUnion = cy.collection()
return
}
let totalUnion = null
for (const newNode of newNodes) {
selectedNodes.add(newNode)
const union = newNode.outgoers().union(newNode.incomers())
totalUnion = totalUnion == null ? union : totalUnion.union(union)
totalUnion = totalUnion.union(newNode)
newNode.addClass("found")
}
cy.elements().difference(totalUnion).addClass("semiTransparent")
totalUnion.addClass("highlight")
this.totalUnion = totalUnion
cy.animate({
// pan: totalUnion.boundingBox(),
// center: {eles: totalUnion},
fit: {eles: totalUnion},
})
}
}
function shortenPath(p) {
const prefix = "plugins/"
if (p.startsWith(prefix)) {
p = p.substring(prefix.length)
}
return p
.replace("/resources/META-INF/", " ")
.replace("/src/main/resources/", " ")
.replace("/META-INF/", " ")
.replace("/resources/", " ")
.replace("/java/src/main/", " ")
.replace("/src/main/", " ")
.replace("/src/", " ")
}

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Plugin Graph</title>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
<script type="module" src="/main.js"></script>
</head>
<body>
<div class="searchFieldContainer">
<!--suppress HtmlFormInputWithoutLabel -->
<input id="searchField" type="search" placeholder="filter"
spellcheck="false"
autofocus/>
</div>
<div id="tooltip"></div>
<div id="cy"></div>
</body>
</html>

151
docs/plugin-graph/main.js Normal file
View File

@@ -0,0 +1,151 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
import "@fontsource/jetbrains-mono/400.css"
import "@fontsource/jetbrains-mono/600.css"
import "./style.css"
import graphData from "./plugin-graph.json"
import cytoscape from "cytoscape"
// noinspection SpellCheckingInspection
import fCose from "cytoscape-fcose"
import {GraphTextSearch} from "./src/GraphTextSearch"
import {GraphHighlighter} from "./src/GraphHighlighter"
import {NodeTooltipManager} from "./src/NodeTooltipManager"
import popper from "cytoscape-popper"
cytoscape.use(fCose)
cytoscape.use(popper)
function listener() {
document.fonts.load("13px 'JetBrains Mono'", "a").then(function () {
initGraph(graphData)
})
}
if (document.readyState === "loading") {
window.addEventListener("DOMContentLoaded", listener)
}
else {
listener()
}
function getItemSizeStyle(factor) {
const baseNodeDiameter = 10
const size = `${Math.ceil(baseNodeDiameter * factor)}px`
return {
"width": size,
"height": size,
}
}
function initGraph(graph) {
// noinspection SpellCheckingInspection
const layoutOptions = {
name: "fcose",
quality: "proof",
randomize: false,
nodeDimensionsIncludeLabels: true,
}
// noinspection SpellCheckingInspection
const cy = cytoscape({
container: document.getElementById("cy"),
elements: graph,
layout: layoutOptions,
minZoom: 0.4,
maxZoom: 3,
autoungrabify: true,
style: [
{
selector: "node",
style: {
"label": "data(n)",
"font-family": "JetBrains Mono",
"font-size": 13,
"color": "#515151",
},
},
{
selector: "node[type=0]",
style: getItemSizeStyle(1),
},
{
selector: "node[type=1]",
style: getItemSizeStyle(1.2),
},
{
selector: "node[type=2]",
style: getItemSizeStyle(1.4),
},
{
selector: "edge",
style: {
"curve-style": "straight",
"width": 1,
}
},
{
selector: "edge[type=0]",
style: {
"target-arrow-shape": "triangle-backcurve",
"arrow-scale": 0.8,
},
},
{
selector: "edge[type=1]",
style: {
"target-arrow-shape": "square",
// square is too big
"arrow-scale": 0.5,
},
},
// highlighting (https://stackoverflow.com/a/38468892)
{
selector: "node.semiTransparent",
style: {"opacity": "0.5"}
},
{
selector: "edge.highlight",
style: {"mid-target-arrow-color": "#FFF"}
},
{
selector: "edge.semiTransparent",
style: {"opacity": "0.2"}
},
{
selector: "node.found",
style: {
"font-weight": "600",
},
},
]
})
// ensure that dragging of element causes panning and not selecting
// https://github.com/cytoscape/cytoscape.js/issues/1905
cy.elements().panify()
function debounce(func) {
let timeout
return function (...args) {
clearTimeout(timeout)
const handler = function() {
func.apply(null, args)
}
timeout = setTimeout(handler, 300)
}
}
const search = new GraphTextSearch(graph, cy)
document.getElementById("searchField").addEventListener("input", debounce(function (event) {
search.searchNodes(event.target.value.trim())
}))
new GraphHighlighter(cy, search)
new NodeTooltipManager(cy)
}

View File

@@ -0,0 +1,20 @@
{
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build --outDir ../out/plugin-graph --base /plugin-graph/ --emptyOutDir",
"serve": "vite preview"
},
"devDependencies": {
"vite": "^2.3.6"
},
"dependencies": {
"@fontsource/jetbrains-mono": "^4.4.2",
"cytoscape": "^3.19.0",
"cytoscape-fcose": "^2.0.0",
"cytoscape-popper": "^2.0.0",
"flexsearch": "^0.6.32",
"tippy.js": "^6.3.1"
}
}

171
docs/plugin-graph/pnpm-lock.yaml generated Normal file
View File

@@ -0,0 +1,171 @@
lockfileVersion: 5.3
specifiers:
'@fontsource/jetbrains-mono': ^4.4.2
cytoscape: ^3.19.0
cytoscape-fcose: ^2.0.0
cytoscape-popper: ^2.0.0
flexsearch: ^0.6.32
tippy.js: ^6.3.1
vite: ^2.3.6
dependencies:
'@fontsource/jetbrains-mono': 4.4.2
cytoscape: 3.19.0
cytoscape-fcose: 2.0.0_cytoscape@3.19.0
cytoscape-popper: 2.0.0_cytoscape@3.19.0
flexsearch: 0.6.32
tippy.js: 6.3.1
devDependencies:
vite: 2.3.6
packages:
/@fontsource/jetbrains-mono/4.4.2:
resolution: {integrity: sha512-5kDjpcnQFogrgGulmES5xX80cDd628+69zSE0ClTqzOAYI43yTyH3MeAH4mxohN6MR7v7AQ9bn+OOFvUuuYwBA==}
dev: false
/@popperjs/core/2.9.2:
resolution: {integrity: sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q==}
dev: false
/colorette/1.2.2:
resolution: {integrity: sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==}
dev: true
/cose-base/2.0.0:
resolution: {integrity: sha512-SdE/oR+5SmdxI5lflXiD9RsUfJ78bXbAzpKkMuK890wVa2PTHQGl1pVwpNca81PWQArM4lNdYmLOt1gCyOdfbg==}
dependencies:
layout-base: 2.0.0
dev: false
/cytoscape-fcose/2.0.0_cytoscape@3.19.0:
resolution: {integrity: sha512-Wy80mbn50qKba5KH2GhZRPZYZGvGXCVK3yWcxSxrZOMJwXmoFQqp76OLUASgdSk5jPsrH3XA+O/bW+hJ5I0Dzw==}
peerDependencies:
cytoscape: ^3.2.0
dependencies:
cose-base: 2.0.0
cytoscape: 3.19.0
dev: false
/cytoscape-popper/2.0.0_cytoscape@3.19.0:
resolution: {integrity: sha512-b7WSOn8qXHWtdIXFNmrgc8qkaOs16tMY0EwtRXlxzvn8X+al6TAFrUwZoYATkYSlotfd/36ZMoeKMEoUck6feA==}
peerDependencies:
cytoscape: ^3.2.0
dependencies:
'@popperjs/core': 2.9.2
cytoscape: 3.19.0
dev: false
/cytoscape/3.19.0:
resolution: {integrity: sha512-DANM8bM4EuSTS3DraPK0nLSmzANrzQ2g/cb3u5Biexu/NVsJA+xAoXVvWHRIHDXz3y0gpMjTPv3Z13ZbkNrEPw==}
engines: {node: '>=0.10'}
dependencies:
heap: 0.2.6
lodash.debounce: 4.0.8
dev: false
/esbuild/0.12.5:
resolution: {integrity: sha512-vcuP53pA5XiwUU4FnlXM+2PnVjTfHGthM7uP1gtp+9yfheGvFFbq/KyuESThmtoHPUrfZH5JpxGVJIFDVD1Egw==}
hasBin: true
requiresBuild: true
dev: true
/flexsearch/0.6.32:
resolution: {integrity: sha512-EF1BWkhwoeLtbIlDbY/vDSLBen/E5l/f1Vg7iX5CDymQCamcx1vhlc3tIZxIDplPjgi0jhG37c67idFbjg+v+Q==}
dev: false
/fsevents/2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
dev: true
optional: true
/function-bind/1.1.1:
resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
dev: true
/has/1.0.3:
resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
engines: {node: '>= 0.4.0'}
dependencies:
function-bind: 1.1.1
dev: true
/heap/0.2.6:
resolution: {integrity: sha1-CH4fELBGky/IWU3Z5tN4r8nR5aw=}
dev: false
/is-core-module/2.4.0:
resolution: {integrity: sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==}
dependencies:
has: 1.0.3
dev: true
/layout-base/2.0.0:
resolution: {integrity: sha512-I3y9zwMl7/ZaGCeQMUwBVABQ0Vz5c1DFlmMtcON4H/E1sBx+RFOcd7SH8nmkOZ0L2wfVPr68MXvtTZB93vT+2w==}
dev: false
/lodash.debounce/4.0.8:
resolution: {integrity: sha1-gteb/zCmfEAF/9XiUVMArZyk168=}
dev: false
/nanoid/3.1.23:
resolution: {integrity: sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
dev: true
/path-parse/1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
dev: true
/postcss/8.3.0:
resolution: {integrity: sha512-+ogXpdAjWGa+fdYY5BQ96V/6tAo+TdSSIMP5huJBIygdWwKtVoB5JWZ7yUd4xZ8r+8Kvvx4nyg/PQ071H4UtcQ==}
engines: {node: ^10 || ^12 || >=14}
dependencies:
colorette: 1.2.2
nanoid: 3.1.23
source-map-js: 0.6.2
dev: true
/resolve/1.20.0:
resolution: {integrity: sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==}
dependencies:
is-core-module: 2.4.0
path-parse: 1.0.7
dev: true
/rollup/2.50.6:
resolution: {integrity: sha512-6c5CJPLVgo0iNaZWWliNu1Kl43tjP9LZcp6D/tkf2eLH2a9/WeHxg9vfTFl8QV/2SOyaJX37CEm9XuGM0rviUg==}
engines: {node: '>=10.0.0'}
hasBin: true
optionalDependencies:
fsevents: 2.3.2
dev: true
/source-map-js/0.6.2:
resolution: {integrity: sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==}
engines: {node: '>=0.10.0'}
dev: true
/tippy.js/6.3.1:
resolution: {integrity: sha512-JnFncCq+rF1dTURupoJ4yPie5Cof978inW6/4S6kmWV7LL9YOSEVMifED3KdrVPEG+Z/TFH2CDNJcQEfaeuQww==}
dependencies:
'@popperjs/core': 2.9.2
dev: false
/vite/2.3.6:
resolution: {integrity: sha512-fsEpNKDHgh3Sn66JH06ZnUBnIgUVUtw6ucDhlOj1CEqxIkymU25yv1/kWDPlIjyYHnalr0cN6V+zzUJ+fmWHYw==}
engines: {node: '>=12.0.0'}
hasBin: true
dependencies:
esbuild: 0.12.5
postcss: 8.3.0
resolve: 1.20.0
rollup: 2.50.6
optionalDependencies:
fsevents: 2.3.2
dev: true

View File

@@ -0,0 +1,36 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
export class GraphHighlighter {
constructor(cy, graphTextSearch) {
this.cy = cy
this.graphTextSearch = graphTextSearch
cy.on("mouseover", "node", e => {
this.selectNode(e.target)
})
cy.on("mouseout", "node", e => {
this.deselectNode(e.target)
})
}
selectNode(selection) {
const cy = this.cy
if (!this.graphTextSearch.totalUnion.empty()) {
cy.elements().difference(this.graphTextSearch.totalUnion).removeClass("semiTransparent")
}
const toHighlight = selection.outgoers().union(selection.incomers()).union(selection)
cy.elements().difference(toHighlight).difference(this.graphTextSearch.totalUnion).addClass("semiTransparent")
toHighlight.difference(this.graphTextSearch.totalUnion).addClass("highlight")
}
deselectNode(selection) {
const cy = this.cy
cy.elements().removeClass("semiTransparent")
if (!this.graphTextSearch.totalUnion.empty()) {
cy.elements().difference(this.graphTextSearch.totalUnion).addClass("semiTransparent")
}
selection.outgoers().union(selection.incomers()).union(selection).difference(this.graphTextSearch.totalUnion).removeClass("highlight")
}
}

View File

@@ -0,0 +1,75 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
import FlexSearch from "flexsearch"
export class GraphTextSearch {
constructor(graph, cy) {
this.cy = cy
this.selectedNodes = new Set()
this.totalUnion = cy.collection()
this.index = new FlexSearch({
tokenize: "strict",
depth: 3,
doc: {
id: "data:id",
field: [
"data:name",
"data:pluginId",
"data:sourceModule",
"data:package",
]
}
})
this.index.add(graph)
}
searchNodes(text) {
const {selectedNodes, cy, index} = this
const newNodes = []
if (text.length !== 0) {
for (const item of index.search(text)) {
const node = cy.getElementById(item.data.id)
if (node == null) {
console.error(`Cannot find node by id ${item.data.id}`)
}
selectedNodes.delete(node)
newNodes.push(node)
}
}
cy.elements().removeClass("semiTransparent")
for (const prevNode of selectedNodes) {
prevNode.removeClass(["highlight", "found"])
prevNode.outgoers().union(prevNode.incomers()).removeClass("highlight")
}
selectedNodes.clear()
if (newNodes.length === 0) {
this.totalUnion = cy.collection()
return
}
let totalUnion = null
for (const newNode of newNodes) {
selectedNodes.add(newNode)
const union = newNode.outgoers().union(newNode.incomers())
totalUnion = totalUnion == null ? union : totalUnion.union(union)
totalUnion = totalUnion.union(newNode)
newNode.addClass("found")
}
cy.elements().difference(totalUnion).addClass("semiTransparent")
totalUnion.addClass("highlight")
this.totalUnion = totalUnion
cy.animate({
// pan: totalUnion.boundingBox(),
// center: {eles: totalUnion},
fit: {eles: totalUnion},
})
}
}

View File

@@ -0,0 +1,90 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
import Tippy from "tippy.js"
export class NodeTooltipManager {
constructor(cy) {
this.tippy = null
cy.on("tap", "node", function (event) {
const node = event.target
const ref = node.popperRef()
if (this.tippy == null) {
const host = document.getElementById("tooltip")
this.tippy = new Tippy(host, {
getReferenceClientRect: ref.getBoundingClientRect,
trigger: "manual",
appendTo: host,
interactive: true,
allowHTML: true,
content: buildTooltipContent(node.data()),
})
}
else {
this.tippy.setProps({
getReferenceClientRect: ref.getBoundingClientRect,
content: buildTooltipContent(node.data()),
})
}
this.tippy.show()
})
}
}
function buildTooltipContent(item) {
const isPackageSet = item.package != null && item.package.length !== 0
// for us is very important to understand dependencies between source modules, that's why on grap source module name is used
// for plugins as node name
const lines = [
{name: item.name, value: null, main: true},
{name: "package", value: isPackageSet ? item.package : "not set", extraStyle: isPackageSet ? null : "color: orange"},
]
if (item.pluginId !== undefined) {
lines.push({name: "pluginId", value: item.pluginId})
}
lines.push(
{name: "sourceModule", value: item.sourceModule},
{name: "descriptor", value: shortenPath(item.descriptor), hint: item.descriptor},
)
return buildTooltip(lines)
}
function buildTooltip(lines) {
let result = ""
for (const line of lines) {
if (line.main) {
result += `<span class="tooltipMainName">${line.name}</span>`
}
else {
result += `<br/><span style="user-select: none">${line.name}</span>`
}
const valueStyleClass = "tooltipValue"
if (line.value != null) {
result += `<span class="${valueStyleClass}"`
if (line.extraStyle != null && line.extraStyle.length > 0) {
result += ` style="${line.extraStyle}"`
}
if (line.hint != null && line.hint.length !== 0) {
result += ` title="${line.hint}"`
}
result += `>${line.value}</span>`
}
}
return result
}
function shortenPath(p) {
const prefix = "plugins/"
if (p.startsWith(prefix)) {
p = p.substring(prefix.length)
}
return p
.replace("/resources/META-INF/", " ")
.replace("/src/main/resources/", " ")
.replace("/META-INF/", " ")
.replace("/resources/", " ")
.replace("/java/src/main/", " ")
.replace("/src/main/", " ")
.replace("/src/", " ")
}

View File

@@ -0,0 +1,48 @@
/* Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. */
html, body {
height: 100%;
}
#cy {
width: 100%;
height: 100%;
}
.tooltipMainName {
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
user-select: text;
}
.tooltipValue {
float: right;
margin-left: 10px;
user-select: text;
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
}
.tippy-box {
line-height: 1.2;
background: rgba(255, 255, 255, 0.9);
color: #515151;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
padding: 10px;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
text-align: justify;
border: 1px solid #ebeef5;
width: fit-content;
max-width: 1000px !important;
}
.searchFieldContainer {
position: absolute;
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
text-align: center;
z-index: 100;
}

View File

@@ -6,6 +6,7 @@
<excludeFolder url="file://$MODULE_DIR$/android/android-uitests" />
<excludeFolder url="file://$MODULE_DIR$/config" />
<excludeFolder url="file://$MODULE_DIR$/system" />
<excludeFolder url="file://$MODULE_DIR$/docs/out/plugin-graph" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />

View File

@@ -81,6 +81,7 @@ internal class PluginGraphWriter(private val pluginIdToInfo: Map<String, ModuleI
writer.obj("data") {
writer.writeStringField("id", id)
writer.writeStringField("name", nodeName)
writer.writeStringField("n", getShortName(nodeName))
writer.writeStringField("package", item.packageName)
writer.writeStringField("sourceModule", item.sourceModuleName)
writer.writeStringField("descriptor", pathToShortString(item.descriptorFile).replace(File.separatorChar, '/'))
@@ -114,6 +115,18 @@ internal class PluginGraphWriter(private val pluginIdToInfo: Map<String, ModuleI
dependencyLinks.computeIfAbsent(dependentId) { mutableListOf() }.add(nodeInfoToId.get(dep)!!)
}
}
private fun getShortName(name: String): String {
if (name.startsWith("intellij.")) {
return "i.${name.substring("intellij.".length)}"
}
else if (name.startsWith("com.intellij.modules.")) {
return "c.i.m.${name.substring("com.intellij.modules.".length)}"
}
else {
return name
}
}
}
private fun writeLinks(writer: JsonGenerator, links: Map<String, List<String>>, isContent: Boolean) {