Compare commits
6 Commits
5fcfcd3f4a
...
d991bef7ab
Author | SHA1 | Date |
---|---|---|
Alexander Klee | d991bef7ab | |
Alexander Klee | 8b3f840c87 | |
Alexander Klee | 09355188b1 | |
Alexander Klee | 217979e9d7 | |
Alexander Klee | 9936fa3f78 | |
Alexander Klee | a819f9dd72 |
16
README.md
16
README.md
|
@ -1,13 +1,21 @@
|
|||
# S-DOM
|
||||
S-DOM is a plugin for IntelliJ IDEA that allows you to submit your code to the DOMjudge system directly from the IDE.
|
||||
It isn't a full replacement for the web interface yet, but submitting files and reading basic results already works better than with that.
|
||||
It isn't a full replacement for the web interface yet, but already matches or surpasses the web UI in most tasks.
|
||||
|
||||
## TODO
|
||||
## Features
|
||||
- Simple, one-click submissions and feedback (no need for File Explorer or reloading!)
|
||||
- View problems and problem statements without leaving your IDE
|
||||
- Unobtrusive and integrated
|
||||
|
||||
## Potential future features
|
||||
- Support detecting the language of files (don't just force C++)
|
||||
- Implement reading detailed results
|
||||
- Implement reading problems (and testcases)
|
||||
- Show scoreboards
|
||||
- Maybe even download test cases and scaffold sources
|
||||
|
||||
## References
|
||||
- [DOMjudge](https://www.domjudge.org/documentation)
|
||||
- [DOMjudge API](https://domjudge.iti.kit.edu/main/api/doc)
|
||||
- [SimpleCodeTester plugin](https://github.com/Mr-Pine/SimpleCodeTester-IntelliJ-Plugin)
|
||||
- [Example Route](https://domjudge.iti.kit.edu/main/api/v4/contests/5/problems)
|
||||
- [Example Route](https://domjudge.iti.kit.edu/main/api/v4/contests/5/problems)
|
||||
- [All IDEA UI Icons](https://jetbrains.design/intellij/resources/icons_list/)
|
|
@ -18,11 +18,9 @@ import io.ktor.client.plugins.contentnegotiation.*
|
|||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.Base64
|
||||
|
@ -164,10 +162,10 @@ object SDom {
|
|||
}
|
||||
|
||||
suspend fun getProblems() {
|
||||
if (currentContest == null) return
|
||||
val contest = currentContest ?: return
|
||||
val result = authenticatedClient.get(SDCredentials.url) {
|
||||
url {
|
||||
appendPathSegments("contests", currentContest!!.id, "problems")
|
||||
appendPathSegments("contests", contest.id, "problems")
|
||||
}
|
||||
}
|
||||
if (result.status.isSuccess()) {
|
||||
|
@ -179,13 +177,24 @@ object SDom {
|
|||
}
|
||||
judgementTypes = authenticatedClient.get(SDCredentials.url) {
|
||||
url {
|
||||
appendPathSegments("contests", currentContest!!.id, "judgement-types")
|
||||
appendPathSegments("contests", contest.id, "judgement-types")
|
||||
}
|
||||
}
|
||||
.body<List<SDJudgementType>>()
|
||||
.associateBy { it.id }
|
||||
}
|
||||
|
||||
suspend fun downloadProblemStatement(contest: Contest, problem: Problem): ByteArray? {
|
||||
val response: ByteArray = authenticatedClient.get(SDCredentials.url) {
|
||||
url {
|
||||
appendPathSegments("contests", contest.id, "problems", problem.id, "statement")
|
||||
}
|
||||
contentType(ContentType.Application.Pdf)
|
||||
}.body()
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
suspend fun submitSolution(
|
||||
contest: Contest, problem: Problem, solution: String, fileName: String
|
||||
): SharedFlow<Result<SDJudgement>> = coroutineScope {
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
package io.gitlab.jfronny.sdom.actions
|
||||
|
||||
import com.intellij.notification.NotificationGroupManager
|
||||
import com.intellij.notification.NotificationType
|
||||
import com.intellij.openapi.actionSystem.ActionUpdateThread
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.fileEditor.FileEditorManager
|
||||
import com.intellij.openapi.project.DumbAwareAction
|
||||
import com.intellij.ui.jcef.JBCefApp
|
||||
import io.gitlab.jfronny.sdom.SDom
|
||||
import io.gitlab.jfronny.sdom.SDom.currentContest
|
||||
import io.gitlab.jfronny.sdom.SDom.currentProblem
|
||||
import io.gitlab.jfronny.sdom.ui.ByteVirtualFile
|
||||
import io.gitlab.jfronny.sdom.util.OSUtils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import java.nio.file.Files
|
||||
|
||||
class SDStatementAction(name: String) : DumbAwareAction(name) {
|
||||
constructor() : this("")
|
||||
|
||||
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
|
||||
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val contest = currentContest
|
||||
val problem = currentProblem
|
||||
if (contest == null) {
|
||||
NotificationGroupManager.getInstance()
|
||||
.getNotificationGroup("sdom.notifications")
|
||||
.createNotification("You have to select a contest", NotificationType.ERROR)
|
||||
.setTitle("No contest selected")
|
||||
.addAction(SDStatementAction("Retry"))
|
||||
.notify(e.project)
|
||||
return
|
||||
}
|
||||
if (problem == null) {
|
||||
NotificationGroupManager.getInstance()
|
||||
.getNotificationGroup("sdom.notifications")
|
||||
.createNotification("You have to select a problem", NotificationType.ERROR)
|
||||
.setTitle("No problem selected")
|
||||
.addAction(SDStatementAction("Retry"))
|
||||
.notify(e.project)
|
||||
return
|
||||
}
|
||||
CoroutineScope(Job() + Dispatchers.IO).launch {
|
||||
val data = SDom.downloadProblemStatement(contest, problem)
|
||||
if (data == null) {
|
||||
NotificationGroupManager.getInstance()
|
||||
.getNotificationGroup("sdom.notifications")
|
||||
.createNotification("This problem lacks a problem statement", NotificationType.ERROR)
|
||||
.setTitle("No problem statement")
|
||||
.addAction(SDStatementAction("Retry"))
|
||||
.notify(e.project)
|
||||
return@launch
|
||||
}
|
||||
if (JBCefApp.isSupported()) {
|
||||
val virtualFile = ByteVirtualFile("${problem.name}_${contest.name}.pdf", data)
|
||||
ApplicationManager.getApplication().invokeLater {
|
||||
FileEditorManager.getInstance(e.project!!).openFile(virtualFile)
|
||||
}
|
||||
} else {
|
||||
val path = Files.createTempFile("sdom", ".pdf")
|
||||
Files.newOutputStream(path).use { it.write(data) }
|
||||
|
||||
OSUtils.openFile(path.toFile())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package io.gitlab.jfronny.sdom.ui
|
||||
|
||||
import com.intellij.openapi.fileTypes.FileType
|
||||
import com.intellij.testFramework.LightVirtualFileBase
|
||||
import com.intellij.util.LocalTimeCounter
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
class ByteVirtualFile(name: String, fileType: FileType?, modificationStamp: Long, private val content: ByteArray) : LightVirtualFileBase(name, fileType, modificationStamp) {
|
||||
constructor() : this("")
|
||||
constructor(name: String) : this(name, ByteArray(0))
|
||||
constructor(name: String, content: ByteArray) : this(name, null, LocalTimeCounter.currentTime(), content)
|
||||
constructor(name: String, fileType: FileType?, content: ByteArray) : this(name, fileType, LocalTimeCounter.currentTime(), content)
|
||||
|
||||
override fun getOutputStream(requestor: Any?, newModificationStamp: Long, newTimeStamp: Long): OutputStream = ByteArrayOutputStream()
|
||||
override fun contentsToByteArray(): ByteArray = this.content.copyOf()
|
||||
override fun getInputStream(): InputStream = ByteArrayInputStream(content)
|
||||
override fun getLength(): Long = content.size.toLong()
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package io.gitlab.jfronny.sdom.ui
|
||||
|
||||
import com.intellij.openapi.fileEditor.FileEditor
|
||||
import com.intellij.openapi.fileEditor.FileEditorLocation
|
||||
import com.intellij.openapi.fileEditor.FileEditorState
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.openapi.util.UserDataHolderBase
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.ui.jcef.JBCefBrowser
|
||||
import com.intellij.ui.jcef.JBCefBrowserBuilder
|
||||
import com.intellij.ui.jcef.JBCefClient
|
||||
import io.gitlab.jfronny.sdom.util.pdfBootstrap
|
||||
import org.jetbrains.annotations.Nls
|
||||
import java.beans.PropertyChangeListener
|
||||
import javax.swing.JComponent
|
||||
|
||||
class SDBrowserViewer(private val file: VirtualFile, cefClient: JBCefClient) : UserDataHolderBase(), FileEditor {
|
||||
private val browser: JBCefBrowser = JBCefBrowserBuilder().setClient(cefClient)
|
||||
.setEnableOpenDevToolsMenuItem(false)
|
||||
.build()
|
||||
|
||||
init {
|
||||
browser.loadHTML(pdfBootstrap(file.contentsToByteArray()))
|
||||
}
|
||||
|
||||
private val component = browser.component
|
||||
|
||||
override fun getComponent(): JComponent {
|
||||
return component
|
||||
}
|
||||
|
||||
override fun getPreferredFocusedComponent(): JComponent? {
|
||||
return component
|
||||
}
|
||||
|
||||
override fun getName(): @Nls(capitalization = Nls.Capitalization.Title) String {
|
||||
return "S-dom Integrated Statement Viewer"
|
||||
}
|
||||
|
||||
override fun setState(fileEditorState: FileEditorState) {
|
||||
}
|
||||
|
||||
override fun isModified(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun isValid(): Boolean {
|
||||
return file.isValid
|
||||
}
|
||||
|
||||
override fun addPropertyChangeListener(propertyChangeListener: PropertyChangeListener) {
|
||||
}
|
||||
|
||||
override fun removePropertyChangeListener(propertyChangeListener: PropertyChangeListener) {
|
||||
}
|
||||
|
||||
override fun getCurrentLocation(): FileEditorLocation? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
Disposer.dispose(this)
|
||||
}
|
||||
|
||||
override fun getFile(): VirtualFile? {
|
||||
return this.file
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package io.gitlab.jfronny.sdom.ui
|
||||
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.fileEditor.FileEditor
|
||||
import com.intellij.openapi.fileEditor.FileEditorPolicy
|
||||
import com.intellij.openapi.fileEditor.FileEditorProvider
|
||||
import com.intellij.openapi.project.DumbAware
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.testFramework.LightVirtualFileBase
|
||||
import com.intellij.ui.jcef.JBCefApp
|
||||
import org.jetbrains.annotations.NonNls
|
||||
|
||||
class SDBrowserViewerProvider : FileEditorProvider, DumbAware {
|
||||
private val ourCefClient = JBCefApp.getInstance().createClient()
|
||||
|
||||
init {
|
||||
Disposer.register(ApplicationManager.getApplication(), ourCefClient)
|
||||
}
|
||||
|
||||
override fun accept(project: Project, virtualFile: VirtualFile): Boolean {
|
||||
return virtualFile is LightVirtualFileBase && virtualFile.extension == "pdf"
|
||||
}
|
||||
|
||||
override fun createEditor(project: Project, virtualFile: VirtualFile): FileEditor {
|
||||
return SDBrowserViewer(virtualFile, ourCefClient)
|
||||
}
|
||||
|
||||
override fun getEditorTypeId(): @NonNls String {
|
||||
return EDITOR_TYPE_ID
|
||||
}
|
||||
|
||||
override fun getPolicy(): FileEditorPolicy {
|
||||
return FileEditorPolicy.HIDE_DEFAULT_EDITOR
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EDITOR_TYPE_ID = "SdomBrowserViewer"
|
||||
}
|
||||
}
|
|
@ -9,6 +9,8 @@ import io.gitlab.jfronny.sdom.SDom
|
|||
import io.gitlab.jfronny.sdom.model.Contest
|
||||
import io.gitlab.jfronny.sdom.model.Problem
|
||||
import io.gitlab.jfronny.sdom.model.SDJudgement
|
||||
import io.gitlab.jfronny.sdom.util.OutputType
|
||||
import io.gitlab.jfronny.sdom.util.println
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import java.awt.BorderLayout
|
||||
import javax.swing.JComponent
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package io.gitlab.jfronny.sdom.ui
|
||||
package io.gitlab.jfronny.sdom.util
|
||||
|
||||
import com.intellij.execution.ui.ConsoleView
|
||||
import com.intellij.execution.ui.ConsoleViewContentType
|
|
@ -0,0 +1,51 @@
|
|||
package io.gitlab.jfronny.sdom.util
|
||||
|
||||
import java.awt.Desktop
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.util.regex.Pattern
|
||||
import java.util.stream.Stream
|
||||
|
||||
object OSUtils {
|
||||
var TYPE: Type? = System.getProperty("os.name", "generic").lowercase().let { os ->
|
||||
if ((os.contains("mac")) || (os.contains("darwin"))) {
|
||||
Type.MAC_OS
|
||||
} else if (os.contains("win")) {
|
||||
Type.WINDOWS
|
||||
} else if (os.contains("nux")) {
|
||||
Type.LINUX
|
||||
} else {
|
||||
null // probably fine :)
|
||||
}
|
||||
}
|
||||
|
||||
fun executablePathContains(executableName: String): Boolean {
|
||||
return try {
|
||||
Stream.of(
|
||||
*System.getenv("PATH").split(Pattern.quote(File.pathSeparator).toRegex()).dropLastWhile { it.isEmpty() }
|
||||
.toTypedArray())
|
||||
.map { path: String -> path.replace("\"", "") }
|
||||
.map { first: String -> Paths.get(first) }
|
||||
.anyMatch { path: Path ->
|
||||
(Files.exists(path.resolve(executableName))
|
||||
&& Files.isExecutable(path.resolve(executableName)))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun openFile(path: File) {
|
||||
if (OSUtils.TYPE === OSUtils.Type.LINUX && OSUtils.executablePathContains("xdg-open")) {
|
||||
Runtime.getRuntime().exec(arrayOf("xdg-open", path.absolutePath))
|
||||
} else if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
||||
Desktop.getDesktop().open(path)
|
||||
}
|
||||
}
|
||||
|
||||
enum class Type {
|
||||
WINDOWS, MAC_OS, LINUX
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
package io.gitlab.jfronny.sdom.util
|
||||
|
||||
import com.intellij.ui.jcef.JBCefScrollbarsHelper
|
||||
import org.intellij.lang.annotations.Language
|
||||
import java.util.Base64
|
||||
|
||||
@Language("HTML")
|
||||
fun pdfBootstrap(pdf: ByteArray) = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<script src="https://mozilla.github.io/pdf.js/build/pdf.mjs" type="module"></script>
|
||||
<link rel='stylesheet' type='text/css' href='https://mozilla.github.io/pdf.js/web/viewer.css'>
|
||||
<style type='text/css'>
|
||||
${JBCefScrollbarsHelper.getOverlayScrollbarStyle()}
|
||||
</style>
|
||||
|
||||
<script type="module">
|
||||
const pdfData = atob('${Base64.getEncoder().encodeToString(pdf)}');
|
||||
|
||||
// Loaded via <script> tag, create shortcut to access PDF.js exports.
|
||||
const { pdfjsLib } = globalThis;
|
||||
|
||||
// The workerSrc property shall be specified.
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://mozilla.github.io/pdf.js/build/pdf.worker.mjs';
|
||||
|
||||
// Using DocumentInitParameters object to load binary data.
|
||||
const loadingTask = pdfjsLib.getDocument({data: pdfData});
|
||||
loadingTask.promise.then(function(pdf) {
|
||||
console.log('PDF loaded');
|
||||
|
||||
const body = document.getElementById('our-body');
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
pdf.getPage(i).then(function(page) {
|
||||
console.log('Page loaded');
|
||||
|
||||
const scale = 1.5;
|
||||
const viewport = page.getViewport({scale: scale});
|
||||
|
||||
// Prepare canvas using PDF page dimensions
|
||||
const canvas = document.createElement('canvas');
|
||||
const textLayer = document.createElement('div');
|
||||
textLayer.className = "textLayer"
|
||||
body.appendChild(canvas)
|
||||
body.appendChild(textLayer)
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
// Render PDF page into canvas context
|
||||
const renderContext = {
|
||||
canvasContext: context,
|
||||
viewport: viewport
|
||||
};
|
||||
const renderTask = page.render(renderContext);
|
||||
renderTask.promise.then(function () {
|
||||
return page.getTextContent()
|
||||
}).then(function (textContent) {
|
||||
|
||||
pdfjsLib.renderTextLayer({
|
||||
textContentSource: textContent,
|
||||
container: textLayer,
|
||||
viewport: viewport,
|
||||
textDivs: []
|
||||
});
|
||||
textLayer.style.left = canvas.offsetLeft + 'px';
|
||||
textLayer.style.top = canvas.offsetTop + 'px';
|
||||
textLayer.style.height = canvas.offsetHeight + 'px';
|
||||
textLayer.style.width = canvas.offsetWidth + 'px';
|
||||
});
|
||||
});
|
||||
}
|
||||
}, function (reason) {
|
||||
// PDF loading error
|
||||
console.error(reason);
|
||||
});
|
||||
</script>
|
||||
<div id="our-body" style="width: 100%" />
|
||||
</body>
|
||||
</html>
|
||||
""".trimIndent()
|
|
@ -25,6 +25,7 @@
|
|||
<!-- Extension points defined by the plugin.
|
||||
Read more: https://plugins.jetbrains.com/docs/intellij/plugin-extension-points.html -->
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
<fileEditorProvider implementation="io.gitlab.jfronny.sdom.ui.SDBrowserViewerProvider"/>
|
||||
<toolWindow factoryClass="io.gitlab.jfronny.sdom.toolwindow.SDToolWindowFactory"
|
||||
id="S-DOM" anchor="bottom" canCloseContents="true" icon="io.gitlab.jfronny.sdom.icons.SDIcons.ToolWindow" />
|
||||
<notificationGroup id="sdom.notifications" displayType="BALLOON" />
|
||||
|
@ -58,6 +59,12 @@
|
|||
text="Refresh Problems">
|
||||
<add-to-group group-id="io.gitlab.jfronny.sdom.actions.SDToolbarActions"/>
|
||||
</action>
|
||||
<action id="io.gitlab.jfronny.sdom.actions.SDStatementAction"
|
||||
class="io.gitlab.jfronny.sdom.actions.SDStatementAction"
|
||||
icon="com.intellij.icons.ExpUiIcons.Toolwindow.Documentation"
|
||||
text="Open Statement">
|
||||
<add-to-group group-id="io.gitlab.jfronny.sdom.actions.SDToolbarActions"/>
|
||||
</action>
|
||||
<action id="io.gitlab.jfronny.sdom.actions.SDSubmitAction"
|
||||
class="io.gitlab.jfronny.sdom.actions.SDSubmitAction"
|
||||
icon="com.intellij.icons.AllIcons.Actions.Upload"
|
||||
|
|
Loading…
Reference in New Issue