* add package.json Signed-off-by: Jess Frazelle <github@jessfraz.com> initial run; Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> more fixes Signed-off-by: Jess Frazelle <github@jessfraz.com> clientsidescne Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> paths Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> fix styles Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> combine Signed-off-by: Jess Frazelle <github@jessfraz.com> eslint rule Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> fixes Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> my ocd Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> constants file Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> no more import sceneInfra Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> try fix circular import Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> --------- Signed-off-by: Jess Frazelle <github@jessfraz.com>
295 lines
9.0 KiB
TypeScript
295 lines
9.0 KiB
TypeScript
/* eslint suggest-no-throw/suggest-no-throw: 0 */
|
|
import * as os from 'os'
|
|
import * as path from 'path'
|
|
import * as vscode from 'vscode'
|
|
import * as Is from 'vscode-languageclient/lib/common/utils/is'
|
|
|
|
import { expectNotUndefined, unwrapUndefinable } from './undefinable'
|
|
import { type Env, log } from './util'
|
|
|
|
export type RunnableEnvCfgItem = {
|
|
mask?: string
|
|
env: Record<string, string>
|
|
platform?: string | string[]
|
|
}
|
|
export type RunnableEnvCfg =
|
|
| undefined
|
|
| Record<string, string>
|
|
| RunnableEnvCfgItem[]
|
|
|
|
export class Config {
|
|
readonly extensionId = 'kittycad.kcl-language-server'
|
|
configureLang: vscode.Disposable | undefined
|
|
|
|
readonly rootSection = 'kcl-language-server'
|
|
private readonly requiresReloadOpts = ['serverPath', 'server', 'files'].map(
|
|
(opt) => `${this.rootSection}.${opt}`
|
|
)
|
|
|
|
readonly package: {
|
|
version: string
|
|
releaseTag: string | null
|
|
enableProposedApi: boolean | undefined
|
|
} = vscode.extensions.getExtension(this.extensionId)!.packageJSON
|
|
|
|
readonly globalStorageUri: vscode.Uri
|
|
|
|
constructor(ctx: vscode.ExtensionContext) {
|
|
this.globalStorageUri = ctx.globalStorageUri
|
|
vscode.workspace.onDidChangeConfiguration(
|
|
this.onDidChangeConfiguration,
|
|
this,
|
|
ctx.subscriptions
|
|
)
|
|
this.refreshLogging()
|
|
}
|
|
|
|
dispose() {
|
|
this.configureLang?.dispose()
|
|
}
|
|
|
|
private refreshLogging() {
|
|
log.setEnabled(this.traceExtension ?? false)
|
|
log.info('Extension version:', this.package.version)
|
|
|
|
const cfg = Object.entries(this.cfg).filter(
|
|
([_, val]) => !(val instanceof Function)
|
|
)
|
|
log.info('Using configuration', Object.fromEntries(cfg))
|
|
}
|
|
|
|
private async onDidChangeConfiguration(
|
|
event: vscode.ConfigurationChangeEvent
|
|
) {
|
|
this.refreshLogging()
|
|
|
|
const requiresReloadOpt = this.requiresReloadOpts.find((opt) =>
|
|
event.affectsConfiguration(opt)
|
|
)
|
|
|
|
if (!requiresReloadOpt) return
|
|
|
|
const message = `Changing "${requiresReloadOpt}" requires a server restart`
|
|
const userResponse = await vscode.window.showInformationMessage(
|
|
message,
|
|
'Restart now'
|
|
)
|
|
|
|
if (userResponse) {
|
|
const command = 'kcl-language-server.restartServer'
|
|
await vscode.commands.executeCommand(command)
|
|
}
|
|
}
|
|
|
|
// We don't do runtime config validation here for simplicity. More on stackoverflow:
|
|
// https://stackoverflow.com/questions/60135780/what-is-the-best-way-to-type-check-the-configuration-for-vscode-extension
|
|
|
|
private get cfg(): vscode.WorkspaceConfiguration {
|
|
return vscode.workspace.getConfiguration(this.rootSection)
|
|
}
|
|
|
|
/**
|
|
* Beware that postfix `!` operator erases both `null` and `undefined`.
|
|
* This is why the following doesn't work as expected:
|
|
*
|
|
* ```ts
|
|
* const nullableNum = vscode
|
|
* .workspace
|
|
* .getConfiguration
|
|
* .getConfiguration("kcl-language-server")
|
|
* .get<number | null>(path)!;
|
|
*
|
|
* // What happens is that type of `nullableNum` is `number` but not `null | number`:
|
|
* const fullFledgedNum: number = nullableNum;
|
|
* ```
|
|
* So this getter handles this quirk by not requiring the caller to use postfix `!`
|
|
*/
|
|
private get<T>(path: string): T | undefined {
|
|
return prepareVSCodeConfig(this.cfg.get<T>(path))
|
|
}
|
|
|
|
get serverPath() {
|
|
return (
|
|
this.get<null | string>('server.path') ??
|
|
this.get<null | string>('serverPath')
|
|
)
|
|
}
|
|
|
|
get traceExtension() {
|
|
return this.get<boolean>('trace.extension')
|
|
}
|
|
}
|
|
|
|
// the optional `cb?` parameter is meant to be used to add additional
|
|
// key/value pairs to the VS Code configuration. This needed for, e.g.,
|
|
// including a `rust-project.json` into the `linkedProjects` key as part
|
|
// of the configuration/InitializationParams _without_ causing VS Code
|
|
// configuration to be written out to workspace-level settings. This is
|
|
// undesirable behavior because rust-project.json files can be tens of
|
|
// thousands of lines of JSON, most of which is not meant for humans
|
|
// to interact with.
|
|
export function prepareVSCodeConfig<T>(
|
|
resp: T,
|
|
cb?: (key: Extract<keyof T, string>, res: { [key: string]: any }) => void
|
|
): T {
|
|
if (Is.string(resp)) {
|
|
return substituteVSCodeVariableInString(resp) as T
|
|
} else if (resp && Is.array<any>(resp)) {
|
|
return resp.map((val) => {
|
|
return prepareVSCodeConfig(val)
|
|
}) as T
|
|
} else if (resp && typeof resp === 'object') {
|
|
const res: { [key: string]: any } = {}
|
|
for (const key in resp) {
|
|
const val = resp[key]
|
|
res[key] = prepareVSCodeConfig(val)
|
|
if (cb) {
|
|
cb(key, res)
|
|
}
|
|
}
|
|
return res as T
|
|
}
|
|
return resp
|
|
}
|
|
|
|
// FIXME: Merge this with `substituteVSCodeVariables` above
|
|
export function substituteVariablesInEnv(env: Env): Env {
|
|
const missingDeps = new Set<string>()
|
|
// vscode uses `env:ENV_NAME` for env vars resolution, and it's easier
|
|
// to follow the same convention for our dependency tracking
|
|
const definedEnvKeys = new Set(Object.keys(env).map((key) => `env:${key}`))
|
|
const envWithDeps = Object.fromEntries(
|
|
Object.entries(env).map(([key, value]) => {
|
|
const deps = new Set<string>()
|
|
const depRe = new RegExp(/\${(?<depName>.+?)}/g)
|
|
let match = undefined
|
|
while ((match = depRe.exec(value))) {
|
|
const depName = unwrapUndefinable(match.groups?.['depName'])
|
|
deps.add(depName)
|
|
// `depName` at this point can have a form of `expression` or
|
|
// `prefix:expression`
|
|
if (!definedEnvKeys.has(depName)) {
|
|
missingDeps.add(depName)
|
|
}
|
|
}
|
|
return [`env:${key}`, { deps: [...deps], value }]
|
|
})
|
|
)
|
|
|
|
const resolved = new Set<string>()
|
|
for (const dep of missingDeps) {
|
|
const match = /(?<prefix>.*?):(?<body>.+)/.exec(dep)
|
|
if (match) {
|
|
const { prefix, body } = match.groups!
|
|
if (prefix === 'env') {
|
|
const envName = unwrapUndefinable(body)
|
|
envWithDeps[dep] = {
|
|
value: process.env[envName] ?? '',
|
|
deps: [],
|
|
}
|
|
resolved.add(dep)
|
|
} else {
|
|
// we can't handle other prefixes at the moment
|
|
// leave values as is, but still mark them as resolved
|
|
envWithDeps[dep] = {
|
|
value: '${' + dep + '}',
|
|
deps: [],
|
|
}
|
|
resolved.add(dep)
|
|
}
|
|
} else {
|
|
envWithDeps[dep] = {
|
|
value: computeVscodeVar(dep) || '${' + dep + '}',
|
|
deps: [],
|
|
}
|
|
}
|
|
}
|
|
const toResolve = new Set(Object.keys(envWithDeps))
|
|
|
|
let leftToResolveSize
|
|
do {
|
|
leftToResolveSize = toResolve.size
|
|
for (const key of toResolve) {
|
|
const item = unwrapUndefinable(envWithDeps[key])
|
|
if (item.deps.every((dep) => resolved.has(dep))) {
|
|
item.value = item.value.replace(
|
|
/\${(?<depName>.+?)}/g,
|
|
(_wholeMatch, depName) => {
|
|
const item = unwrapUndefinable(envWithDeps[depName])
|
|
return item.value
|
|
}
|
|
)
|
|
resolved.add(key)
|
|
toResolve.delete(key)
|
|
}
|
|
}
|
|
} while (toResolve.size > 0 && toResolve.size < leftToResolveSize)
|
|
|
|
const resolvedEnv: Env = {}
|
|
for (const key of Object.keys(env)) {
|
|
const item = unwrapUndefinable(envWithDeps[`env:${key}`])
|
|
resolvedEnv[key] = item.value
|
|
}
|
|
return resolvedEnv
|
|
}
|
|
|
|
const VarRegex = new RegExp(/\$\{(.+?)\}/g)
|
|
function substituteVSCodeVariableInString(val: string): string {
|
|
return val.replace(VarRegex, (substring: string, varName) => {
|
|
if (Is.string(varName)) {
|
|
return computeVscodeVar(varName) || substring
|
|
} else {
|
|
return substring
|
|
}
|
|
})
|
|
}
|
|
|
|
function computeVscodeVar(varName: string): string | null {
|
|
const workspaceFolder = () => {
|
|
const folders = vscode.workspace.workspaceFolders ?? []
|
|
const folder = folders[0]
|
|
// TODO: support for remote workspaces?
|
|
const fsPath: string =
|
|
folder === undefined
|
|
? // no workspace opened
|
|
''
|
|
: // could use currently opened document to detect the correct
|
|
// workspace. However, that would be determined by the document
|
|
// user has opened on Editor startup. Could lead to
|
|
// unpredictable workspace selection in practice.
|
|
// It's better to pick the first one
|
|
folder.uri.fsPath
|
|
return fsPath
|
|
}
|
|
// https://code.visualstudio.com/docs/editor/variables-reference
|
|
const supportedVariables: { [k: string]: () => string } = {
|
|
workspaceFolder,
|
|
|
|
workspaceFolderBasename: () => {
|
|
return path.basename(workspaceFolder())
|
|
},
|
|
|
|
cwd: () => process.cwd(),
|
|
userHome: () => os.homedir(),
|
|
|
|
// see
|
|
// https://github.com/microsoft/vscode/blob/08ac1bb67ca2459496b272d8f4a908757f24f56f/src/vs/workbench/api/common/extHostVariableResolverService.ts#L81
|
|
// or
|
|
// https://github.com/microsoft/vscode/blob/29eb316bb9f154b7870eb5204ec7f2e7cf649bec/src/vs/server/node/remoteTerminalChannel.ts#L56
|
|
execPath: () => process.env['VSCODE_EXEC_PATH'] ?? process.execPath,
|
|
|
|
pathSeparator: () => path.sep,
|
|
}
|
|
|
|
if (varName in supportedVariables) {
|
|
const fn = expectNotUndefined(
|
|
supportedVariables[varName],
|
|
`${varName} should not be undefined here`
|
|
)
|
|
return fn()
|
|
} else {
|
|
// return "${" + varName + "}";
|
|
return null
|
|
}
|
|
}
|