Skip to content

Commit

Permalink
feat: if Dark IntelliJ theme is used, use dark D2 theme for preview b…
Browse files Browse the repository at this point in the history
…y default

Close #1
  • Loading branch information
develar committed Dec 31, 2023
1 parent 9ba8599 commit 34ce304
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 132 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,18 @@ import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.components.service
import com.intellij.openapi.fileChooser.FileChooserFactory
import com.intellij.openapi.fileChooser.FileSaverDescriptor
import com.intellij.openapi.fileEditor.FileEditor
import com.intellij.openapi.util.io.FileUtilRt
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.plugins.d2.D2Bundle
import org.jetbrains.plugins.d2.d2FileEditor
import org.jetbrains.plugins.d2.editor.D2Service
import org.jetbrains.plugins.d2.execution.D2Command
import org.jetbrains.plugins.d2.editor.D2Viewer
import org.jetbrains.plugins.d2.editor.GenerateCommand
import java.nio.file.Files

enum class ConversionOutput { SVG, PNG, JPG, TIFF }

private fun getGeneratedCommand(fileEditor: FileEditor): D2Command.Generate? = service<D2Service>().map.get(fileEditor)?.command
private fun getGeneratedCommand(fileEditor: D2Viewer): GenerateCommand? = service<D2Service>().map.get(fileEditor)

@OptIn(ExperimentalStdlibApi::class)
private class D2ExportAction : AnAction() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ private class D2LayoutEngineAction(private val layout: D2Layout) : ToggleAction(

override fun setSelected(e: AnActionEvent, state: Boolean) {
e.d2FileEditor.putUserData(D2_FILE_LAYOUT, layout)
service<D2Service>().compile(e.d2FileEditor)
service<D2Service>().compileAndWatch(e.d2FileEditor)
}

override fun getActionUpdateThread() = ActionUpdateThread.BGT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ private class D2MessagesAction : AnAction(), DumbAware {
Disposer.register(content, console)
}

val console = content.component as ConsoleViewImpl
val console = content.component.getComponent(0) as ConsoleViewImpl
console.clear()
messageView.contentManager.setSelectedContent(content)
for (command in service<D2Service>().map.values) {
console.print(command.log, ConsoleViewContentType.LOG_INFO_OUTPUT)
console.print(command.log.toString(), ConsoleViewContentType.LOG_INFO_OUTPUT)
}

val toolWindow = ToolWindowManager.getInstance(project).getToolWindow(ToolWindowId.MESSAGES_WINDOW)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ private class D2ThemeAction(private val theme: D2Theme) : ToggleAction(theme.tNa

override fun setSelected(e: AnActionEvent, state: Boolean) {
e.d2FileEditor.putUserData(D2_FILE_THEME, theme)
service<D2Service>().compile(e.d2FileEditor)
service<D2Service>().compileAndWatch(e.d2FileEditor)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ private class D2FileEditorProvider : FileEditorProvider, DumbAware {
}

override fun createEditor(project: Project, file: VirtualFile): FileEditor {
val view = D2SvgViewer(project, file)
val view = D2Viewer(project, file)
val editor = TextEditorProvider.getInstance().createEditor(project, file) as TextEditor
return TextEditorWithPreview(editor, view, D2_EDITOR_NAME, TextEditorWithPreview.Layout.SHOW_EDITOR_AND_PREVIEW)
}
Expand Down
215 changes: 138 additions & 77 deletions src/main/kotlin/org/jetbrains/plugins/d2/editor/D2Service.kt
Original file line number Diff line number Diff line change
@@ -1,29 +1,35 @@
package org.jetbrains.plugins.d2.editor

import com.dvd.intellij.d2.components.D2Layout
import com.dvd.intellij.d2.components.D2Theme
import com.dvd.intellij.d2.ide.action.ConversionOutput
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.*
import com.intellij.ide.ui.LafManagerListener
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.Service
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.fileEditor.FileEditor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream
import com.intellij.openapi.util.removeUserData
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.ui.EditorNotifications
import com.intellij.ui.JBColor
import com.intellij.util.EnvironmentUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.plugins.d2.D2Bundle
import org.jetbrains.plugins.d2.execution.D2Command
import org.jetbrains.plugins.d2.execution.D2CommandOutput
import java.io.File
import java.net.ServerSocket
import java.nio.file.Files
import java.nio.file.Path
import java.util.concurrent.CancellationException
import java.util.concurrent.ConcurrentHashMap
import java.util.function.Supplier
import javax.imageio.ImageIO
import kotlin.system.measureTimeMillis
Expand All @@ -34,9 +40,17 @@ internal val D2_FILE_NOTIFICATION: Key<Supplier<String>> = Key("d2EditorNotifica

@Service
class D2Service(private val coroutineScope: CoroutineScope) : Disposable {
private val editorToState: MutableMap<FileEditor, D2CommandOutput.Generate> = HashMap()
private val viewerToState = ConcurrentHashMap<D2Viewer, GenerateCommand>()

val map: Map<FileEditor, D2CommandOutput.Generate> = editorToState
val map: Map<D2Viewer, GenerateCommand> = viewerToState

init {
ApplicationManager.getApplication().messageBus.connect(coroutineScope).subscribe(LafManagerListener.TOPIC, LafManagerListener {
for (viewer in java.util.List.copyOf(viewerToState.keys)) {
compileAndWatch(viewer)
}
})
}

fun getCompilerVersion(): String? {
try {
Expand All @@ -52,7 +66,7 @@ class D2Service(private val coroutineScope: CoroutineScope) : Disposable {

fun getLayoutEngines(): List<D2Layout>? = executeAndGetOutputOrNull(D2Command.LayoutEngines)?.layouts

fun scheduleCompile(fileEditor: D2SvgViewer, project: Project) {
fun scheduleCompile(fileEditor: D2Viewer, project: Project) {
coroutineScope.launch {
if (!fileEditor.isValid) {
return@launch
Expand All @@ -64,7 +78,7 @@ class D2Service(private val coroutineScope: CoroutineScope) : Disposable {

try {
withContext(Dispatchers.IO) {
compile(fileEditor)
compileAndWatch(fileEditor)
}
if (fileEditor.removeUserData(D2_FILE_NOTIFICATION) != null) {
EditorNotifications.getInstance(project).updateNotifications(fileEditor.file)
Expand All @@ -84,63 +98,74 @@ class D2Service(private val coroutineScope: CoroutineScope) : Disposable {
}
}

fun compile(fileEditor: D2SvgViewer) {
val oldExec = editorToState.get(fileEditor)
val oldCommand = oldExec?.command

val theme = fileEditor.getUserData(D2_FILE_THEME)
val layout = fileEditor.getUserData(D2_FILE_LAYOUT)
val command = if (oldCommand == null) {
// find a free port
val port = ServerSocket(0).use {
it.localPort
}
fun compileAndWatch(fileEditor: D2Viewer) {
val command: GenerateCommand
synchronized(fileEditor) {
val oldCommand = viewerToState.get(fileEditor)

val theme = fileEditor.getUserData(D2_FILE_THEME)
val layout = fileEditor.getUserData(D2_FILE_LAYOUT)
val targetFile = Files.createTempFile("d2_temp_svg", ".svg")
D2Command.Generate(input = fileEditor.file, targetFile = targetFile, port = port, theme = theme, layout = layout)
} else {
oldCommand.copy(theme = theme, layout = layout)
}
if (oldCommand == null) {
// find a free port
val port = ServerSocket(0).use {
it.localPort
}

oldCommand?.process?.let {
it.destroyProcess()
@Suppress("ControlFlowWithEmptyBody")
val terminationTime = measureTimeMillis {
// background process? ~5ms
while (!it.isProcessTerminated) {
command = GenerateCommand(
input = fileEditor.file,
targetFile = targetFile,
port = port,
theme = theme,
layout = layout,
log = StringBuilder("[plugin ] info: starting process...\n"),
)
} else {
oldCommand.process?.let {
it.destroyProcess()
@Suppress("ControlFlowWithEmptyBody")
val terminationTime = measureTimeMillis {
// background process? ~5ms
while (!it.isProcessTerminated) {
}
}
oldCommand.log.append("[plugin ] [info] D2 process termination ${terminationTime}ms\n")
}
}
"[plugin ] [info] D2 process termination ${terminationTime}ms".let { message ->
editorToState.put(fileEditor, editorToState.get(fileEditor)?.appendLog(message) ?: return)
LOG.info(message)
}
}
command.process = prepare(command, object : ProcessListener {
override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) {
onTextAvailable(fileEditor = fileEditor, event = event, outputType = outputType)
}

override fun processWillTerminate(event: ProcessEvent, willBeDestroyed: Boolean) {
deleteFile(command)
command = GenerateCommand(
input = fileEditor.file,
targetFile = targetFile,
port = oldCommand.port,
theme = theme,
layout = layout,
log = StringBuilder(oldCommand.log).append("[plugin ] info: restarting process...\n"),
)
}
})

@Suppress("IfThenToElvis")
editorToState[fileEditor] = if (oldExec == null) {
command.parseOutput("[plugin ] info: starting process...\n")
} else {
oldExec.copy(command = command).appendLog("[plugin ] info: restarting process...\n")
command.process = prepare(command, object : ProcessListener {
override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) {
onTextAvailable(event = event, outputType = outputType, log = command.log)
}

override fun processWillTerminate(event: ProcessEvent, willBeDestroyed: Boolean) {
command.deleteTargetFile()
}
})

viewerToState.put(fileEditor, command)
}
command.process?.startNotify()

command.process?.startNotify()
fileEditor.refreshD2(command.port)
}

fun closeFile(fileEditor: FileEditor) {
fun closeFile(fileEditor: D2Viewer) {
fileEditor.putUserData(D2_FILE_LAYOUT, null)
fileEditor.putUserData(D2_FILE_THEME, null)

editorToState.remove(fileEditor)?.command?.process?.destroyProcess()
synchronized(fileEditor) {
viewerToState.remove(fileEditor)
}?.process?.destroyProcess()
}

fun format(file: File): D2FormatterResult {
Expand Down Expand Up @@ -171,43 +196,36 @@ class D2Service(private val coroutineScope: CoroutineScope) : Disposable {
return out.toByteArray()
}

private fun onTextAvailable(fileEditor: FileEditor, event: ProcessEvent, outputType: Key<*>) {
buildString {
append("[process] ")
if (outputType == ProcessOutputType.SYSTEM) {
append("info: ")
append(event.text)
} else {
// remove timestamp
append(event.text.replace(Regex("\\[?\\d{2}:\\d{2}:\\d{2}]? "), ""))
}
}.let {
LOG.info(it)

// null if file editor closed
editorToState[fileEditor] = editorToState[fileEditor]?.appendLog(it) ?: return
}
}

override fun dispose() {
for (item in editorToState.values) {
deleteFile(item.command)
for (command in viewerToState.values) {
command.deleteTargetFile()
}
editorToState.clear()
viewerToState.clear()
}

private fun deleteFile(command: D2Command.Generate) {
command.targetFile.let { Files.deleteIfExists(it) }
}

private fun prepare(command: D2Command<*>, listener: ProcessListener?) =
KillableColoredProcessHandler.Silent(command.createCommandLine().apply {
withEnvironment(command.envVars())
}).apply {
private fun prepare(command: GenerateCommand, listener: ProcessListener?): KillableColoredProcessHandler.Silent {
val commandLine = GeneralCommandLine(command.getArgs())
.withCharset(Charsets.UTF_8)
.withEnvironment(command.envVars())
return KillableColoredProcessHandler.Silent(commandLine).apply {
if (listener != null) {
addProcessListener(listener)
}
}
}
}

private val timestampRegexp = Regex("\\[?\\d{2}:\\d{2}:\\d{2}]? ")

private fun onTextAvailable(event: ProcessEvent, outputType: Key<*>, log: StringBuilder) {
log.append("[process] ")
if (outputType == ProcessOutputType.SYSTEM) {
log.append("info: ")
log.append(event.text)
} else {
// remove timestamp
log.append(event.text.replace(timestampRegexp, ""))
}
}

// null if d2 executable not found
Expand All @@ -228,3 +246,46 @@ private fun <O> executeAndGetOutput(command: D2Command<O>): O? {
)
return command.parseOutput(processOut)
}

class GenerateCommand(
val input: VirtualFile,
val targetFile: Path,
val port: Int,
val theme: D2Theme?,
val layout: D2Layout?,
val log: StringBuilder,
) {
var process: ProcessHandler? = null

fun deleteTargetFile() {
targetFile.let { Files.deleteIfExists(it) }
}

fun getArgs(): List<String> = buildList {
add("d2")

add("--watch")

add("--port")
add(port.toString())

if (layout != null) {
add("--layout")
add(layout.name)
}

if (theme != null) {
add("--theme")
add(theme.id.toString())
} else if (!JBColor.isBright()) {
// https://github.com/develar/d2-intellij-plugin/issues/1
add("--theme")
add(EnvironmentUtil.getValue("D2_DARK_THEME")?.takeIf { it.isNotBlank() } ?: "200")
}

add(input.path)
add(targetFile.toString())
}

fun envVars(): Map<String, String> = java.util.Map.of("BROWSER", "0")
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ val D2_FILE_LAYOUT: Key<D2Layout> = Key<D2Layout>("d2_file_layout")
val D2_FILE_THEME: Key<D2Theme> = Key<D2Theme>("d2_file_theme")

// https://github.com/JetBrains/intellij-community/blob/master/images/src/org/intellij/images/editor/impl/ImageFileEditorImpl.java
class D2SvgViewer(
class D2Viewer(
val project: Project,
private val file: VirtualFile
) : UserDataHolderBase(), FileEditor {
Expand Down
Loading

0 comments on commit 34ce304

Please sign in to comment.