Improved client-server integration

This commit is contained in:
Noah Santschi-Cooney 2020-07-17 15:31:27 +01:00
parent 17c4f8aa93
commit 39143aefc7
No known key found for this signature in database
GPG key ID: 3B22282472C8AE48
18 changed files with 1794 additions and 1105 deletions

23
client/src/commands.ts Normal file
View file

@ -0,0 +1,23 @@
import * as vscode from 'vscode'
import * as lsp from 'vscode-languageclient'
import { Extension } from './extension'
import { log } from './log'
export type Command = (...args: any[]) => unknown
export function generateGraphDot(e: Extension): Command {
return async () => {
await e.lspClient.sendRequest(lsp.ExecuteCommandRequest.type.method, {
command: 'graphDot',
arguments: [vscode.workspace.workspaceFolders[0].uri.path],
})
}
}
export function restartExtension(e: Extension): Command {
return async () => {
vscode.window.showInformationMessage('Reloading Minecraft GLSL language server...')
await e.deactivate()
await e.activate(e.context).catch(log.error)
}
}

View file

@ -1,74 +1,68 @@
import * as path from 'path'
import * as vscode from 'vscode'
import * as vscodeLang from 'vscode-languageclient'
import * as lsp from 'vscode-languageclient'
import * as commands from './commands'
import { promptDownload, testExecutable } from './glslangValidator'
import { log } from './log'
import { LanguageClient } from './lspClient'
export const glslConfigParam = 'mcglsl.glslangValidatorPath'
export let outputChannel: vscode.OutputChannel
export let statusBarItem: vscode.StatusBarItem | null = null
let statusBarItem: vscode.StatusBarItem
let globalContext: vscode.ExtensionContext
export async function activate(context: vscode.ExtensionContext) {
outputChannel = vscode.window.createOutputChannel('vscode-mc-shader')
globalContext = context
if (!testExecutable(vscode.workspace.getConfiguration().get(glslConfigParam))) {
await promptDownload()
} else {
outputChannel.appendLine('glslangValidator found!')
}
const clientOpts: vscodeLang.LanguageClientOptions = {
documentSelector: [{scheme: 'file', language: 'glsl'}],
outputChannel: outputChannel,
outputChannelName: 'vscode-mc-shader',
synchronize: {
configurationSection: 'mcglsl',
fileEvents: vscode.workspace.createFileSystemWatcher('**/*.{fsh,gsh,vsh,glsl}')
},
}
const serverOpts: vscodeLang.ServerOptions = {
command: context.asAbsolutePath(path.join('server', 'target', 'debug', 'vscode-mc-shader')),
}
outputChannel.appendLine('starting language server...')
const langServer = new vscodeLang.LanguageClient('vscode-mc-shader', serverOpts, clientOpts)
context.subscriptions.push(langServer.start())
export class Extension {
private extensionContext: vscode.ExtensionContext | null = null
private client: lsp.LanguageClient
await langServer.onReady()
public get context() : vscode.ExtensionContext {
return this.extensionContext
}
langServer.onNotification('updateConfig', (dir: string) => {
vscode.workspace.getConfiguration().update(glslConfigParam, dir, vscode.ConfigurationTarget.Global)
})
public get lspClient() : lsp.LanguageClient {
return this.client
}
public activate = async (context: vscode.ExtensionContext) => {
this.extensionContext = context
langServer.onNotification('status', updateStatus)
this.registerCommand('graphDot', commands.generateGraphDot)
this.registerCommand('restart', commands.restartExtension)
if (!testExecutable(vscode.workspace.getConfiguration().get(glslConfigParam) as string)) {
if(!await promptDownload(this)) return
}
log.info('starting language server...')
this.client = await new LanguageClient(this).startServer()
log.info('language server started!')
}
langServer.onNotification('clearStatus', clearStatus)
registerCommand = (name: string, f: (e: Extension) => commands.Command) => {
const cmd = f(this)
this.context.subscriptions.push(vscode.commands.registerCommand('mcshader.'+name, cmd))
}
outputChannel.appendLine('language server started!')
context.subscriptions.push(vscode.commands.registerCommand("mcshader.graphDot", async () => {
await langServer.sendRequest(vscodeLang.ExecuteCommandRequest.type.method, {
command: 'graphDot',
arguments: [vscode.workspace.workspaceFolders[0].uri.path],
})
}))
public deactivate = async () => {
await this.lspClient.stop()
while(this.context.subscriptions.length > 0) {
this.context.subscriptions.pop()?.dispose()
}
}
public updateStatus = (icon: string, text: string) => {
statusBarItem?.dispose()
statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left)
statusBarItem.text = icon + ' [mc-shader] ' + text
statusBarItem.show()
this.context.subscriptions.push(statusBarItem)
}
public clearStatus = () => {
statusBarItem?.dispose()
}
}
export function updateStatus(icon: string, text: string) {
if(statusBarItem != null) statusBarItem.dispose()
statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left)
statusBarItem.text = icon + " [Minecraft Shaders] " + text
statusBarItem.show()
globalContext.subscriptions.push(statusBarItem)
}
export function clearStatus() {
if(statusBarItem != null) statusBarItem.dispose()
}
export const activate = new Extension().activate

View file

@ -4,7 +4,8 @@ import { writeFileSync } from 'fs'
import fetch from 'node-fetch'
import { platform } from 'os'
import * as vscode from 'vscode'
import { clearStatus, glslConfigParam, outputChannel, updateStatus } from './extension'
import { Extension, glslConfigParam } from './extension'
import { log } from './log'
const url = {
'win32': 'https://github.com/KhronosGroup/glslang/releases/download/master-tot/glslang-master-windows-x64-Release.zip',
@ -14,55 +15,57 @@ const url = {
const config = vscode.workspace.getConfiguration()
export async function promptDownload() {
export async function promptDownload(e: Extension): Promise<boolean> {
const chosen = await vscode.window.showErrorMessage(
`[mc-glsl] glslangValidator not found at: '${config.get(glslConfigParam)}'.`,
{title: 'Download'},
{title: 'Cancel'}
)
if (!chosen || chosen.title !== 'Download') return
if (!chosen || chosen.title !== 'Download') return false
await installExecutable()
return await tryInstallExecutable(e)
}
async function installExecutable() {
async function tryInstallExecutable(e: Extension): Promise<boolean> {
try {
updateStatus('$(cloud-download)', 'Downloading glslangValidator')
const glslangBin = '/glslangValidator' + (platform() === 'win32' ? '.exe' : '')
const glslangPath = config.get('mcglsl.shaderpacksPath') + glslangBin
const response = await fetch(url[platform()])
outputChannel.appendLine('glslangValidator download response status: ' + response.status )
const zip = new unzip(await response.buffer())
const bin = zip.readFile('bin' + glslangBin)
outputChannel.appendLine('buffer length ' + bin.length)
writeFileSync(glslangPath, bin, {encoding: null, mode: 0o755})
// Make sure download was successful
if (!testExecutable(glslangPath)) {
vscode.window.showErrorMessage(`Unexpected error occurred checking for binary at ${glslangPath}. Please try again`)
clearStatus()
throw new Error('failed to install glslangValidator')
}
// All done!
outputChannel.appendLine(`successfully downloaded glslangValidator to ${glslangPath}`)
vscode.window.showInformationMessage(
`glslangValidator has been downloaded to ${glslangPath}. Your config should be updated automatically.`
)
config.update('mcglsl.glslangValidatorPath', glslangPath, vscode.ConfigurationTarget.Global)
clearStatus()
await installExecutable(e)
} catch (e) {
outputChannel.appendLine(`failed downloading glslangValidator ${e}`)
log.error(`failed downloading glslangValidator ${e}`)
vscode.window.showErrorMessage(`Failed to install glslangValidator: ${e}`)
clearStatus()
throw e
e.clearStatus()
return false
}
return true
}
async function installExecutable(e: Extension) {
e.updateStatus('$(cloud-download)', 'Downloading glslangValidator')
const glslangBin = '/glslangValidator' + (platform() === 'win32' ? '.exe' : '')
const glslangPath = config.get('mcglsl.shaderpacksPath') + glslangBin
const response = await fetch(url[platform()])
log.info('glslangValidator download response status: ' + response.status)
const zip = new unzip(await response.buffer())
const bin = zip.readFile('bin' + glslangBin)
log.info('buffer length ' + bin.length)
writeFileSync(glslangPath, bin, {encoding: null, mode: 0o755})
// Make sure download was successful
if (!testExecutable(glslangPath)) {
throw new Error(`Unexpected error occurred checking for binary at ${glslangPath}. Please try again`)
}
// All done!
log.info(`successfully downloaded glslangValidator to ${glslangPath}`)
vscode.window.showInformationMessage(
`glslangValidator has been downloaded to ${glslangPath}. Your config should be updated automatically.`
)
config.update('mcglsl.glslangValidatorPath', glslangPath, vscode.ConfigurationTarget.Global)
e.clearStatus()
}
export function testExecutable(glslangPath?: string): boolean {
@ -76,13 +79,13 @@ export function testExecutable(glslangPath?: string): boolean {
stdout = (e.stdout.toString() as string)
}
outputChannel.appendLine('glslangValidator first line stdout: "' + stdout.split('\n')[0] + '"')
log.info('glslangValidator first line stdout: "' + stdout.slice(0, stdout.indexOf('\n')) + '"')
const success = stdout.startsWith('Usage')
if (success) {
outputChannel.appendLine(`glslangValidator found at ${glslangPath}`)
log.info(`glslangValidator found at ${glslangPath}`)
} else {
outputChannel.appendLine(`glslangValidator not found at ${glslangPath}`)
log.warn(`glslangValidator not found at ${glslangPath}`)
}
return success

66
client/src/log.ts Normal file
View file

@ -0,0 +1,66 @@
import { inspect } from 'util'
import * as vscode from 'vscode'
// from rust-analyzer https://github.com/rust-analyzer/rust-analyzer/blob/ef223b9e6439c228e0be49861efd2067c0b22af4/editors/code/src/util.ts
export const log = new class {
readonly output = vscode.window.createOutputChannel('Minecraft Shaders');
// Hint: the type [T, ...T[]] means a non-empty array
debug(...msg: [unknown, ...unknown[]]): void {
log.write('DEBUG', ...msg)
}
info(...msg: [unknown, ...unknown[]]): void {
log.write('INFO ', ...msg)
}
warn(...msg: [unknown, ...unknown[]]): void {
log.write('WARN ', ...msg)
}
error(...msg: [unknown, ...unknown[]]): void {
log.write('ERROR', ...msg)
}
write(label: string, ...messageParts: unknown[]): void {
const message = messageParts.map(log.stringify).join(' ')
const dateTime = new Date().toLocaleString()
log.output.appendLine(`${label} [${dateTime}]: ${message}`)
}
private stringify(val: unknown): string {
if (typeof val === 'string') return val
return inspect(val, {
colors: false,
depth: 6, // heuristic
})
}
}
export const lspExceptionLogger = new class implements vscode.OutputChannel {
name: string
append(value: string): void {
log.write('LSP-F', value)
}
appendLine(value: string): void {
log.write('LSP-F', value)
}
clear(): void {
log.output.clear()
}
show(column?: any, preserveFocus?: any) {
log.output.show(column, preserveFocus)
}
hide(): void {
log.output.hide()
}
dispose(): void {
log.output.dispose()
}
}

54
client/src/lspClient.ts Normal file
View file

@ -0,0 +1,54 @@
import * as path from 'path'
import { ConfigurationTarget, workspace } from 'vscode'
import * as lsp from 'vscode-languageclient'
import { Extension } from './extension'
import { lspExceptionLogger } from './log'
import { ConfigUpdateParams, statusMethod, StatusParams, updateConfigMethod } from './lspExt'
export class LanguageClient extends lsp.LanguageClient {
private extension: Extension
constructor(ext: Extension) {
super('vscode-mc-shader', 'VSCode MC Shader', {
command: ext.context.asAbsolutePath(path.join('server', 'target', 'debug', 'vscode-mc-shader')),
}, {
documentSelector: [{scheme: 'file', language: 'glsl'}],
outputChannel: lspExceptionLogger,
synchronize: {
configurationSection: 'mcglsl',
fileEvents: workspace.createFileSystemWatcher('**/*.{fsh,gsh,vsh,glsl}')
},
})
this.extension = ext
}
public startServer = async (): Promise<LanguageClient> => {
this.extension.context.subscriptions.push(this.start())
await this.onReady()
this.onNotification(updateConfigMethod, this.onUpdateConfig)
this.onNotification(statusMethod, this.onStatusChange)
return this
}
onStatusChange = (params: StatusParams) => {
switch (params.status) {
case 'loading':
case 'ready':
case 'failed':
this.extension.updateStatus(params.icon, params.message)
break
case 'clear':
this.extension.clearStatus()
break
}
}
onUpdateConfig = (params: ConfigUpdateParams) => {
for (const kv of params.kv) {
workspace.getConfiguration().update('mcglsl.' + kv.key, kv.value, ConfigurationTarget.Global)
}
}
}

16
client/src/lspExt.ts Normal file
View file

@ -0,0 +1,16 @@
import * as lsp from 'vscode-languageclient'
export type StatusParams = {
status: 'loading' | 'ready' | 'failed' | 'clear'
message: string
icon: string
}
export const statusMethod = 'mc-glsl/status'
export const status = new lsp.NotificationType<StatusParams>(statusMethod)
export const updateConfigMethod = 'mc-glsl/updateConfig'
export type ConfigUpdateParams = {
kv: {key: string, value: string}[]
}