Skip to content

Commit

Permalink
Merge pull request #13 from JetBrains/local-run
Browse files Browse the repository at this point in the history
Add local run capability + Self-Hosted support + UX and UI improvements
  • Loading branch information
evgenyim authored Jun 17, 2024
2 parents cf0ac32 + 1453c85 commit f478614
Show file tree
Hide file tree
Showing 62 changed files with 3,945 additions and 814 deletions.
59 changes: 37 additions & 22 deletions src/main/kotlin/org/jetbrains/qodana/SarifLanguageServer.kt
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
package org.jetbrains.qodana

import org.apache.logging.log4j.LogManager
import org.eclipse.lsp4j.*
import org.eclipse.lsp4j.services.LanguageClient
import org.eclipse.lsp4j.services.LanguageClientAware
import java.util.concurrent.CompletableFuture
import kotlin.system.exitProcess
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.launch
import org.apache.logging.log4j.LogManager
import org.eclipse.lsp4j.*
import org.eclipse.lsp4j.services.LanguageClient
import org.eclipse.lsp4j.services.LanguageClientAware
import org.jetbrains.annotations.VisibleForTesting
import java.nio.file.Path
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap
import kotlin.system.exitProcess

@OptIn(FlowPreview::class)
class SarifLanguageServer: ExtendedLanguageServer, LanguageClientAware {
Expand All @@ -22,7 +26,10 @@ class SarifLanguageServer: ExtendedLanguageServer, LanguageClientAware {
}

private val workspaceService = SarifWorkspaceService()
private var clientCapabilities: ClientCapabilities? = null
private val serverCapabilities = ServerCapabilities().apply {
setTextDocumentSync(TextDocumentSyncKind.Incremental)
diagnosticProvider = DiagnosticRegistrationOptions()
}
private val scope = CoroutineScope(Dispatchers.IO)
private val requestChannel = Channel<IRequest>(UNLIMITED)
private val debouncedChannel = Channel<IRequest>(UNLIMITED)
Expand All @@ -32,7 +39,6 @@ class SarifLanguageServer: ExtendedLanguageServer, LanguageClientAware {
System.getProperty("os.name").lowercase().contains("win"),
scope)
private val textDocumentService = SarifTextDocumentService(state)
private var exitCode = -1
init {
scope.launch {
merge(requestChannel.consumeAsFlow(), debouncedChannel.consumeAsFlow().debounce(DebounceInterval)).collect {
Expand Down Expand Up @@ -73,16 +79,24 @@ class SarifLanguageServer: ExtendedLanguageServer, LanguageClientAware {
return future
}

override fun initialize(params: InitializeParams?): CompletableFuture<InitializeResult> {
logger.info("server/initialize")
return CompletableFuture.supplyAsync {
val capabilities = ServerCapabilities().apply {
setTextDocumentSync(TextDocumentSyncKind.Incremental)
diagnosticProvider = DiagnosticRegistrationOptions()
override fun closeReport(): CompletableFuture<Unit> {
logger.info("server/closeReport")
val future = CompletableFuture<Unit>()

scope.launch {
try {
requestChannel.send(CloseReport())
future.complete(Unit)
} catch (ex: Exception) {
future.completeExceptionally(ex)
}
clientCapabilities = params?.capabilities
InitializeResult(capabilities)
}
return future
}

override fun initialize(params: InitializeParams?): CompletableFuture<InitializeResult> {
logger.info("server/initialize")
return CompletableFuture.completedFuture(InitializeResult(serverCapabilities))
}

override fun setTrace(params: SetTraceParams?) {
Expand All @@ -91,18 +105,19 @@ class SarifLanguageServer: ExtendedLanguageServer, LanguageClientAware {

override fun shutdown(): CompletableFuture<Any> {
logger.info("server/shutdown")
return CompletableFuture.supplyAsync {

}
requestChannel.close()
debouncedChannel.close()
return CompletableFuture.completedFuture(null)
}

override fun exit() {
logger.info("server/exit")
exitProcess(exitCode)
exitProcess(0)
}

override fun connect(client: LanguageClient?) {
logger.info("server/connect")
// logger.info("$client")
state.languageClient = client
}

Expand All @@ -121,7 +136,7 @@ class SarifLanguageServer: ExtendedLanguageServer, LanguageClientAware {
var sarifRevision: Lazy<String?>? = null,
var repoFolder: Path? = null,
var gitLocator: Lazy<GitLocator?>? = null,
val openFileCache: MutableMap<String, String> = mutableMapOf(),
val openFileCache: MutableMap<String, String> = ConcurrentHashMap(),
var repositoryFileCache: MutableMap<String, String>? = null
)
}
10 changes: 10 additions & 0 deletions src/main/kotlin/org/jetbrains/qodana/SarifTextDocumentService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import kotlinx.coroutines.*
import org.apache.logging.log4j.LogManager
import org.eclipse.lsp4j.*
import org.eclipse.lsp4j.services.TextDocumentService
import java.net.URI
import java.net.URL
import java.nio.file.Paths
import java.util.concurrent.CompletableFuture
Expand All @@ -16,6 +17,7 @@ class SarifTextDocumentService(private val state: SarifLanguageServer.ServerStat
override fun didOpen(params: DidOpenTextDocumentParams) {
logger.trace("text/didOpen {fileUri: '${params.textDocument?.uri}'}")
val url = params.textDocument?.uri ?: return
if (!isFileScheme(url)) return
state.scope.launch {
state.requestChannel.send(OpenFile(url, params.textDocument?.text ?: Paths.get(URL(url).toURI()).toFile().readText()))
state.debounceChannel.send(RemapDiagnostics(url))
Expand All @@ -29,6 +31,7 @@ class SarifTextDocumentService(private val state: SarifLanguageServer.ServerStat
override fun didChange(params: DidChangeTextDocumentParams) {
logger.trace("text/didChange {fileUri: '${params.textDocument?.uri}'}")
val url = params.textDocument?.uri ?: return
if (!isFileScheme(url)) return
state.scope.launch {
state.requestChannel.send(UpdateFile(url, params.contentChanges))
state.debounceChannel.send(RemapDiagnostics(url))
Expand All @@ -38,6 +41,7 @@ class SarifTextDocumentService(private val state: SarifLanguageServer.ServerStat
override fun didClose(params: DidCloseTextDocumentParams) {
logger.trace("text/didClose {fileUri: '${params.textDocument?.uri}'}")
val url = params.textDocument?.uri ?: return
if (!isFileScheme(url)) return
state.scope.launch {
state.requestChannel.send(CloseFile(url))
}
Expand All @@ -46,9 +50,15 @@ class SarifTextDocumentService(private val state: SarifLanguageServer.ServerStat
override fun didSave(params: DidSaveTextDocumentParams) {
logger.trace("text/didSave {fileUri: '${params.textDocument?.uri}'}")
val url = params.textDocument?.uri ?: return
if (!isFileScheme(url)) return
state.scope.launch {
state.requestChannel.send(OpenFile(url, Paths.get(URL(url).toURI()).toFile().readText()))
state.debounceChannel.send(RemapDiagnostics(url))
}
}

private fun isFileScheme(uriString: String): Boolean {
val uri = URI(uriString)
return uri.scheme == "file"
}
}
3 changes: 3 additions & 0 deletions src/main/kotlin/org/jetbrains/qodana/extensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ interface ExtendedLanguageServer : LanguageServer {

@JsonRequest
fun setSarifFile(params: SetSarifFileParams) : CompletableFuture<Unit>

@JsonRequest
fun closeReport() : CompletableFuture<Unit>
}

data class SetSourceLocationParams(val path: String)
Expand Down
15 changes: 13 additions & 2 deletions src/main/kotlin/org/jetbrains/qodana/requests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import java.io.FileReader
import java.net.URL
import java.nio.file.Path
import java.nio.file.Paths
import java.util.concurrent.ConcurrentHashMap
import kotlin.io.path.exists
import kotlin.text.StringBuilder

Expand Down Expand Up @@ -49,8 +50,8 @@ class SarifFile(private val path: String, private val showBaselineIssues: Boolea
state.sarifLocation = path
val diags = computeDiagnosticsAsIs(showBaselineIssues)
val oldFiles = state.diagnostic?.keys ?: emptyList()
state.diagnostic = diags.toMap(mutableMapOf())
state.repositoryFileCache = mutableMapOf()
state.diagnostic = diags.toMap(ConcurrentHashMap())
state.repositoryFileCache = ConcurrentHashMap()
state.sarifRevision = lazy { getRevisionId(path) }
state.gitLocator?.value?.close()
state.gitLocator = lazy {
Expand Down Expand Up @@ -267,6 +268,16 @@ class CloseFile(private val uri: String): IRequest {
}
}

class CloseReport : IRequest {
override suspend fun execute(state: SarifLanguageServer.ServerState) {
val oldFiles = state.diagnostic?.keys ?: emptyList()
state.diagnostic = null
for (file in oldFiles) {
state.requestChannel.send(AnnounceDiagnostics(file, emptyList()))
}
}
}


fun getProblems(sarifPath: String): Sequence<Result> = sequence {
FileReader(sarifPath).use { fileReader ->
Expand Down
3 changes: 2 additions & 1 deletion vscode/qodana/.vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"--extensionDevelopmentPath=${workspaceFolder}"
],
"env": {
"QODANA_DEBUG": "true"
"QODANA_DEBUG": "true",
"EXTENSION_DEBUG": "false"
},
"outFiles": [
"${workspaceFolder}/out/**/*.js"
Expand Down
15 changes: 15 additions & 0 deletions vscode/qodana/media/link.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//@ts-check

// This script will be run within the webview itself
// It cannot access the main VS Code APIs directly.
(function () {
const vscode = acquireVsCodeApi();

document.querySelector('.unlink-button').addEventListener('click', () => {
vscode.postMessage({ type: 'unlink' });
});

document.querySelector('.open-report-button').addEventListener('click', () => {
vscode.postMessage({ type: 'openReport' });
});
}());
35 changes: 35 additions & 0 deletions vscode/qodana/media/login.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//@ts-check

// This script will be run within the webview itself
// It cannot access the main VS Code APIs directly.
(function () {
const vscode = acquireVsCodeApi();

document.querySelector('.login-button').addEventListener('click', () => {
vscode.postMessage({ type: myConstants.COMMAND_LOG_IN });
});

document.querySelector('.self-hosted-button').addEventListener('click', () => {
vscode.postMessage({ type: myConstants.COMMAND_LOG_IN_CUSTOM_SERVER });
});

// noinspection DuplicatedCode
document.querySelector('.close-report-button').addEventListener('click', () => {
vscode.postMessage({ type: myConstants.COMMAND_CLOSE_REPORT });
});

window.addEventListener('message', event => {
const message = event.data; // Here 'data' contains information sent from extension.
switch (message.type) {
case 'hide':
let element = document.querySelector(message.data);
if (element && message.visible === false) {
element.classList.add('hide-element');
} else if (element) {
element.classList.remove('hide-element');
}
break;
}
});

}());
7 changes: 7 additions & 0 deletions vscode/qodana/media/main.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
body {
background-color: transparent;
}

.hide-element {
display: none;
}
3 changes: 3 additions & 0 deletions vscode/qodana/media/qodana-bw.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 30 additions & 0 deletions vscode/qodana/media/reset.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
html {
box-sizing: border-box;
font-size: 13px;
}

*,
*:before,
*:after {
box-sizing: inherit;
}

body,
h1,
h2,
h3,
h4,
h5,
h6,
p,
ol,
ul {
margin: 0;
padding: 0;
font-weight: normal;
}

img {
max-width: 100%;
height: auto;
}
15 changes: 15 additions & 0 deletions vscode/qodana/media/runLocally.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//@ts-check

// This script will be run within the webview itself
// It cannot access the main VS Code APIs directly.
(function () {
const vscode = acquireVsCodeApi();

document.querySelector('.run-locally-button').addEventListener('click', () => {
vscode.postMessage({ type: myConstants.COMMAND_RUN_LOCALLY });
});

document.querySelector('.view-history-button').addEventListener('click', () => {
vscode.postMessage({ type: myConstants.COMMAND_OPEN_LOCAL_REPORT });
});
}());
30 changes: 30 additions & 0 deletions vscode/qodana/media/settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//@ts-check

// This script will be run within the webview itself
// It cannot access the main VS Code APIs directly.
(function () {
const vscode = acquireVsCodeApi();

document.querySelector('.logout-button').addEventListener('click', () => {
vscode.postMessage({ type: myConstants.COMMAND_LOG_OUT });
});

// noinspection DuplicatedCode
document.querySelector('.close-report-button').addEventListener('click', () => {
vscode.postMessage({ type: myConstants.COMMAND_CLOSE_REPORT });
});

window.addEventListener('message', event => {
const message = event.data; // Here 'data' contains information sent from extension.
switch (message.type) {
case 'hide':
let element = document.querySelector(message.data);
if (element && message.visible === false) {
element.classList.add('hide-element');
} else if (element) {
element.classList.remove('hide-element');
}
break;
}
});
}());
Loading

0 comments on commit f478614

Please sign in to comment.