mirror of
https://github.com/Strum355/mcshader-lsp.git
synced 2025-07-19 17:26:13 +00:00
162 lines
No EOL
5.1 KiB
TypeScript
162 lines
No EOL
5.1 KiB
TypeScript
import { conf, connection, documents } from './server'
|
|
import { TextDocument, Diagnostic, DiagnosticSeverity, Range } from 'vscode-languageserver'
|
|
import { execSync } from 'child_process'
|
|
import * as path from 'path'
|
|
import { readFileSync } from 'fs'
|
|
|
|
const reDiag = /^(ERROR|WARNING): ([^?<>:*|"]+?):(\d+): (?:'.*?' : )?(.+)$/
|
|
const reVersion = /#version [\d]{3}/
|
|
const reInclude = /^(?:\s)*?(?:#include) "((?:\/?[^?<>:*|"]+?)+?\.(?:[a-zA-Z]+?))"$/
|
|
const include = '#extension GL_GOOGLE_include_directive : require'
|
|
|
|
const filters = [
|
|
/(No code generated)/,
|
|
/(compilation terminated)/,
|
|
/Could not process include directive for header name:/
|
|
]
|
|
|
|
const files = new Map<string, number>()
|
|
|
|
export const ext = new Map([
|
|
['.fsh', 'frag'],
|
|
['.gsh', 'geom'],
|
|
['.vsh', 'vert'],
|
|
])
|
|
|
|
const tokens = new Map([
|
|
['SEMICOLON', ';'],
|
|
['COMMA', ','],
|
|
['COLON', ':'],
|
|
['EQUAL', '='],
|
|
['LEFT_PAREN', '('],
|
|
['RIGHT_PAREN', ')'],
|
|
['DOT', '.'],
|
|
['BANG', '!'],
|
|
['DASH', '-'],
|
|
['TILDE', '~'],
|
|
['PLUS', '+'],
|
|
['STAR', '*'],
|
|
['SLASH', '/'],
|
|
['PERCENT', '%'],
|
|
['LEFT_ANGEL', '<'],
|
|
['RIGHT_ANGEL', '>'],
|
|
['VERICAL_BAR', '|'],
|
|
['CARET', '^'],
|
|
['AMPERSAND', '&'],
|
|
['QUESTION', '?'],
|
|
['[LEFT_BRACKET', '['],
|
|
['RIGHT_BRACKET', ']'],
|
|
['LEFT_BRACE', '{'],
|
|
['RIGHT_BRACE', '}'],
|
|
])
|
|
|
|
// TODO exclude exts not in ext
|
|
export function preprocess(lines: string[], docURI: string, topLevel: boolean, incStack: string[]) {
|
|
if (topLevel) {
|
|
let inComment = false
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i]
|
|
//TODO something better than this
|
|
if (line.includes('/*')) inComment = true
|
|
if (line.includes('*/')) inComment = false
|
|
if (line.trim().startsWith('//')) continue
|
|
if (!inComment && reVersion.test(line)) {
|
|
lines.splice(i + 1, 0, include)
|
|
break
|
|
}
|
|
if (i === lines.length - 1) {
|
|
lines.splice(0, 0, include)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
const includes = getIncludes(lines)
|
|
let addedLines = 0
|
|
if (includes.length > 0) {
|
|
includes.forEach((inc, i) => {
|
|
const incPath = absPath(docURI, inc.match[1])
|
|
const dataLines = readFileSync(incPath).toString().split('\n')
|
|
lines[inc.lineNum + addedLines + i] = `#line 0 "${incPath}"`
|
|
lines.splice(inc.lineNum + 1 + addedLines + i, 0, ...dataLines)
|
|
addedLines += dataLines.length
|
|
lines.splice(inc.lineNum + 1 + addedLines + i, 0, `#line ${inc.lineNum} "${docURI}"`)
|
|
preprocess(lines, incPath, false, incStack)
|
|
})
|
|
}
|
|
|
|
if (!topLevel) return
|
|
|
|
try {
|
|
lint(docURI, lines, includes)
|
|
} catch (e) {
|
|
}
|
|
}
|
|
|
|
export const formatURI = (uri: string) => uri.replace(/^file:\/\//, '')
|
|
|
|
//TODO not include in comments
|
|
const getIncludes = (lines: string[]) => lines
|
|
.map((line, i) => ({num: i, line}))
|
|
.filter(obj => reInclude.test(obj.line))
|
|
.map(obj => ({lineNum: obj.num, match: obj.line.match(reInclude)}))
|
|
|
|
function absPath(currFile: string, includeFile: string): string {
|
|
if (!currFile.startsWith(conf.shaderpacksPath)) {
|
|
connection.window.showErrorMessage(`Shaderpacks path may not be correct. Current file is in ${currFile} but the path is set to ${conf.shaderpacksPath}`)
|
|
return
|
|
}
|
|
|
|
if (includeFile.charAt(0) === '/') {
|
|
const shaderPath = currFile.replace(conf.shaderpacksPath, '').split('/').slice(0, 3).join('/')
|
|
return path.join(conf.shaderpacksPath, shaderPath, includeFile)
|
|
}
|
|
return path.join(path.dirname(currFile), includeFile)
|
|
}
|
|
|
|
function lint(uri: string, lines: string[], includes: {lineNum: number, match: RegExpMatchArray}[]) {
|
|
console.log(lines.join('\n'))
|
|
let out: string = ''
|
|
try {
|
|
execSync(`${conf.glslangPath} --stdin -S ${ext[path.extname(uri)]}`, {input: lines.join('\n')})
|
|
} catch (e) {
|
|
out = e.stdout.toString()
|
|
}
|
|
|
|
const diagnostics: {[uri: string]: Diagnostic[]} = {uri: []}
|
|
includes.forEach(obj => {
|
|
diagnostics[absPath(uri, obj.match[1])] = []
|
|
})
|
|
|
|
const matches = filterMatches(out)
|
|
matches.forEach((match) => {
|
|
const [whole, type, file, line, msg] = match
|
|
const diag = {
|
|
severity: type === 'ERROR' ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning,
|
|
range: calcRange(parseInt(line) - 1, uri),
|
|
message: replaceWord(msg),
|
|
source: 'mc-glsl'
|
|
}
|
|
diagnostics[file ? uri : file].push(diag)
|
|
})
|
|
|
|
daigsArray(diagnostics).forEach(d => connection.sendDiagnostics({uri: d.uri, diagnostics: d.diag}))
|
|
}
|
|
|
|
const replaceWord = (msg: string) => Object.entries(tokens).reduce((acc, [key, value]) => acc.replace(key, value), msg)
|
|
|
|
const daigsArray = (diags: {[uri: string]: Diagnostic[]}) => Object.keys(diags).map(uri => ({uri: 'file://' + uri, diag: diags[uri]}))
|
|
|
|
const filterMatches = (output: string) => output
|
|
.split('\n')
|
|
.filter(s => s.length > 1 && !filters.some(reg => reg.test(s)))
|
|
.map(s => s.match(reDiag))
|
|
.filter(match => match && match.length === 5)
|
|
|
|
function calcRange(lineNum: number, uri: string): Range {
|
|
const lines = documents.get('file://' + uri).getText().split('\n')
|
|
const line = lines[lineNum]
|
|
const startOfLine = line.length - line.trimLeft().length
|
|
const endOfLine = line.slice(0, line.indexOf('//')).trimRight().length + 1
|
|
return Range.create(lineNum, startOfLine, lineNum, endOfLine)
|
|
} |