diff --git a/src/main/kotlin/texlab/LanguageServerImpl.kt b/src/main/kotlin/texlab/LanguageServerImpl.kt index 5c7cf843..a4d41878 100644 --- a/src/main/kotlin/texlab/LanguageServerImpl.kt +++ b/src/main/kotlin/texlab/LanguageServerImpl.kt @@ -42,7 +42,7 @@ class LanguageServerImpl : LanguageServer, CoroutineScope { val capabilities = ServerCapabilities().apply { val syncOptions = TextDocumentSyncOptions().apply { openClose = true - save = SaveOptions(false) + save = SaveOptions(true) change = TextDocumentSyncKind.Full } textDocumentSync = Either.forRight(syncOptions) diff --git a/src/main/kotlin/texlab/TextDocumentServiceImpl.kt b/src/main/kotlin/texlab/TextDocumentServiceImpl.kt index 57d3c10e..26c66d8c 100644 --- a/src/main/kotlin/texlab/TextDocumentServiceImpl.kt +++ b/src/main/kotlin/texlab/TextDocumentServiceImpl.kt @@ -25,6 +25,7 @@ import texlab.completion.latex.data.symbols.LatexSymbolDatabase import texlab.definition.BibtexEntryDefinitionProvider import texlab.definition.LatexLabelDefinitionProvider import texlab.diagnostics.BibtexEntryDiagnosticsProvider +import texlab.diagnostics.LatexDiagnosticsProvider import texlab.diagnostics.ManualDiagnosticsProvider import texlab.folding.BibtexDeclarationFoldingProvider import texlab.folding.LatexEnvironmentFoldingProvider @@ -204,11 +205,13 @@ class TextDocumentServiceImpl(val workspaceActor: WorkspaceActor) : CustomTextDo BibtexEntryReferenceProvider) val buildDiagnosticsProvider: ManualDiagnosticsProvider = ManualDiagnosticsProvider() + private val latexDiagnosticsProvider: LatexDiagnosticsProvider = LatexDiagnosticsProvider() private val diagnosticsProvider: FeatureProvider> = FeatureProvider.concat( buildDiagnosticsProvider, - BibtexEntryDiagnosticsProvider) + BibtexEntryDiagnosticsProvider, + latexDiagnosticsProvider) fun connect(client: CustomLanguageClient) { this.client = client @@ -239,6 +242,7 @@ class TextDocumentServiceImpl(val workspaceActor: WorkspaceActor) : CustomTextDo workspaceActor.put { Document.create(uri, text, language) } launch { + latexDiagnosticsProvider.update(uri, text) publishDiagnostics(uri) resolveIncludes() } @@ -267,6 +271,9 @@ class TextDocumentServiceImpl(val workspaceActor: WorkspaceActor) : CustomTextDo override fun didSave(params: DidSaveTextDocumentParams) { launch { val uri = URIHelper.parse(params.textDocument.uri) + latexDiagnosticsProvider.update(uri, params.text) + publishDiagnostics(uri) + val config = client.configuration("latex.build", uri) if (config.onSave) { workspaceActor.withWorkspace { workspace -> diff --git a/src/main/kotlin/texlab/diagnostics/LatexDiagnosticsProvider.kt b/src/main/kotlin/texlab/diagnostics/LatexDiagnosticsProvider.kt new file mode 100644 index 00000000..ddc251cf --- /dev/null +++ b/src/main/kotlin/texlab/diagnostics/LatexDiagnosticsProvider.kt @@ -0,0 +1,23 @@ +package texlab.diagnostics + +import org.eclipse.lsp4j.Diagnostic +import texlab.LatexDocument +import texlab.provider.FeatureProvider +import texlab.provider.FeatureRequest +import java.net.URI + +class LatexDiagnosticsProvider : FeatureProvider> { + private val diagnosticsByUri = mutableMapOf>() + + override suspend fun get(request: FeatureRequest): List { + if (request.document !is LatexDocument) { + return emptyList() + } + + return diagnosticsByUri[request.uri].orEmpty() + } + + suspend fun update(uri: URI, text: String) { + diagnosticsByUri[uri] = LatexLinter.lint(text) + } +} diff --git a/src/main/kotlin/texlab/diagnostics/LatexLinter.kt b/src/main/kotlin/texlab/diagnostics/LatexLinter.kt new file mode 100644 index 00000000..c655aa14 --- /dev/null +++ b/src/main/kotlin/texlab/diagnostics/LatexLinter.kt @@ -0,0 +1,66 @@ +package texlab.diagnostics + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.future.await +import kotlinx.coroutines.withContext +import org.eclipse.lsp4j.Diagnostic +import org.eclipse.lsp4j.DiagnosticSeverity +import org.eclipse.lsp4j.Position +import org.eclipse.lsp4j.Range +import java.io.IOException +import java.io.InputStream +import java.util.concurrent.CompletableFuture + +object LatexLinter { + private val LINE_REGEX = Regex("""(\d+):(\d+):(\d+):(\w+):(\w)+:(.*)""") + + suspend fun lint(text: String): List { + try { + val process = withContext(Dispatchers.IO) { + ProcessBuilder("chktex", "-I0", "-f%l:%c:%d:%k:%n:%m\n") + .redirectOutput(ProcessBuilder.Redirect.PIPE) + .redirectError(ProcessBuilder.Redirect.PIPE) + .redirectInput(ProcessBuilder.Redirect.PIPE) + .start() + } + + val stderr = readTextAsync(process.errorStream) + val stdout = readTextAsync(process.inputStream) + withContext(Dispatchers.IO) { + process.outputStream.write(text.toByteArray()) + process.outputStream.flush() + process.outputStream.close() + process.waitFor() + } + + stderr.await() + return stdout.await() + .lines() + .filter { it.isNotBlank() } + .mapNotNull { parse(it) } + } catch (e: IOException) { + return emptyList() + } + } + + private fun readTextAsync(stream: InputStream) = CompletableFuture.supplyAsync { + stream.bufferedReader(Charsets.UTF_8).readText() + } + + private fun parse(text: String): Diagnostic? { + val match = LINE_REGEX.matchEntire(text) ?: return null + val line = match.groupValues[1].toInt() - 1 + val character = match.groupValues[2].toInt() - 1 + val digit = match.groupValues[3].toInt() + val kind = match.groupValues[4] + val number = match.groupValues[5] + val message = match.groupValues[6] + val range = Range(Position(line, character), Position(line, character + digit)) + val severity = when (kind) { + "Message" -> DiagnosticSeverity.Information + "Warning" -> DiagnosticSeverity.Warning + else -> DiagnosticSeverity.Error + } + return Diagnostic(range, message, severity, "chktex", number) + } +}