Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 184 additions & 37 deletions package-lock.json

Large diffs are not rendered by default.

43 changes: 30 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,32 @@
"type": "object",
"title": "V",
"properties": {
"v.releaseChannel": {
"type": "string",
"default": "stable",
"enum": [
"stable",
"nightly",
"custom"
],
"description": "Specify the V version to use",
"enumDescriptions": [
"Get the latest stable release of V. (Default)",
"Gets the latest development version of V.",
"Uses a custom version of V."
]
},
"v.buildPath": {
"type": "string",
"default": "",
"description": "Custom path to the V repo. Used only if 'v.releaseChannel' is set to 'custom'."
},
"v.forceCleanInstall": {
"scope": "resource",
"type": "boolean",
"default": false,
"description": "Removes any existing V installation and forces a clean install on startup."
},
"v.vls.command": {
"type": "string",
"default": "v",
Expand All @@ -114,18 +140,6 @@
"type": "boolean",
"description": "Enables / disables the language server's debug mode.\nSetting it to true will create a log file to your workspace folder for bug reports."
},
"v.vls.customVrootPath": {
"scope": "resource",
"type": "string",
"default": "",
"description": "Custom path to the V installation directory (VROOT).\nNOTE: Setting this won't change the V compiler executable to be used."
},
"v.vls.customPath": {
"scope": "resource",
"type": "string",
"default": "",
"description": "Custom path to the VLS (V Language Server) executable."
},
"v.vls.enable": {
"scope": "resource",
"type": "boolean",
Expand All @@ -147,6 +161,7 @@
"v.vls.buildPath": {
"scope": "resource",
"type": "string",
"default": "",
"description": "Path to vls source to build."
},
"v.vls.enableFeatures": {
Expand Down Expand Up @@ -266,15 +281,17 @@
],
"main": "./out/extension.js",
"dependencies": {
"extract-zip": "^2.0.1",
"tar": "^7.5.1",
"vscode-languageclient": "10.0.0-next.15"
},
"devDependencies": {
"esbuild": "^0.25.10",
"@types/node": "24",
"@types/vscode": "1.105.0",
"@typescript-eslint/eslint-plugin": "^8.46",
"@typescript-eslint/parser": "^8.46",
"@vscode/vsce": "^3.6.2",
"esbuild": "^0.25.10",
"eslint": "^9.37.0",
"globals": "^15.9.0",
"markdownlint-cli": "^0.45.0",
Expand Down
9 changes: 6 additions & 3 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,13 @@ export function registerCommands(context: ExtensionContext): Promise<void> {
return Promise.resolve()
}

export function registerVlsCommands(context: ExtensionContext, client?: LanguageClient): void {
export function registerVlsCommands(
context: ExtensionContext,
getClient?: () => LanguageClient | undefined,
): void {
context.subscriptions.push(
commands.registerCommand("v.vls.update", () => updateVls(client)),
commands.registerCommand("v.vls.restart", () => restartVls(client)),
commands.registerCommand("v.vls.update", () => updateVls(getClient?.())),
commands.registerCommand("v.vls.restart", () => restartVls(getClient?.())),
commands.registerCommand("v.vls.openOutput", () => {
vlsOutputChannel.show()
}),
Expand Down
6 changes: 5 additions & 1 deletion src/exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,9 @@ export async function execVInTerminalOnBG(args: string[], cwd = "/"): Promise<vo
const vexec = getVExecCommand()
const cmd = `${vexec} ${args.join(" ")}`

await exec(cmd, { cwd })
try {
await exec(cmd, { cwd })
} catch (error) {
console.error("Error executing command:", error)
}
}
24 changes: 8 additions & 16 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getVls, isVlsEnabled } from "langserver"
import { log, outputChannel, vlsOutputChannel } from "logger"
import vscode, { ConfigurationChangeEvent, ExtensionContext, workspace } from "vscode"
import { LanguageClient, LanguageClientOptions, ServerOptions } from "vscode-languageclient/node"
import { installV, isVInstalled } from "./utils"
import { handleVinstallation } from "./vUtils"

export let client: LanguageClient | undefined

Expand All @@ -18,6 +18,7 @@ async function createAndStartClient(): Promise<void> {
const clientOptions: LanguageClientOptions = {
documentSelector: [{ scheme: "file", language: "v" }],
outputChannel: vlsOutputChannel,
traceOutputChannel: vlsOutputChannel,
synchronize: {
fileEvents: vscode.workspace.createFileSystemWatcher("**/*.v"),
},
Expand All @@ -33,19 +34,7 @@ export async function activate(context: ExtensionContext): Promise<void> {
// Register output channels so users can open them even without VLS.
context.subscriptions.push(outputChannel, vlsOutputChannel)

// Check for V only if it's not installed
if (!(await isVInstalled())) {
const selection = await vscode.window.showInformationMessage(
"The V programming language is not detected on this system. Would you like to install it?",
{ modal: true }, // Modal makes the user have to choose before continuing
"Yes",
"No",
)

if (selection === "Yes") {
await installV()
}
}
await handleVinstallation()

// Register commands regardless of whether VLS is enabled
await registerCommands(context)
Expand All @@ -65,10 +54,10 @@ export async function activate(context: ExtensionContext): Promise<void> {
log("VLS is disabled in settings.")
}

registerVlsCommands(context, client)
registerVlsCommands(context, () => client)

// React to configuration changes: enable/disable or request restart.
workspace.onDidChangeConfiguration(async (e: ConfigurationChangeEvent) => {
const configListener = workspace.onDidChangeConfiguration(async (e: ConfigurationChangeEvent) => {
const vlsEnabled = isVlsEnabled()

if (e.affectsConfiguration("v.vls.enable")) {
Expand Down Expand Up @@ -109,8 +98,11 @@ export async function activate(context: ExtensionContext): Promise<void> {
}
}
})
} else if (e.affectsConfiguration("v.releaseChannel")) {
await handleVinstallation()
}
})
context.subscriptions.push(configListener)
}

export function deactivate(): Promise<void> | undefined {
Expand Down
26 changes: 18 additions & 8 deletions src/langserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ import { exec as _exec } from "child_process"
import { promises as fs } from "fs"
import { promisify } from "util"
import { execVInTerminalOnBG } from "./exec"
import { isVInstalled } from "./utils"
import { isVInstalled } from "./vUtils"

const exec = promisify(_exec)

export const BINARY_NAME = process.platform === "win32" ? "vls.exe" : "vls"

export const USER_BIN_PATH = path.join(os.homedir(), ".local", "bin")

export const VLS_PATH = path.join(USER_BIN_PATH, BINARY_NAME) // ~/.local/bin/vls if not tmp enabled
export const VLS_PATH = path.join(USER_BIN_PATH, BINARY_NAME) // ~/.local/bin/vls

export async function getVls(): Promise<string> {
if (vlsConfig().get<boolean>("forceCleanInstall")) {
await fs.rm(VLS_PATH, { recursive: true, force: true })
await fs.rm(VLS_PATH, { force: true })
log("forceCleanInstall is enabled, removed existing VLS.")
} else if (await isVlsInstalled()) {
// dont check if installed if forceCleanInstall is true
Expand Down Expand Up @@ -65,22 +65,32 @@ export function installVls(): Promise<string> {
}

export async function buildVls(): Promise<string> {
if (!(await isVInstalled())) {
const { installed: isInstalled, version } = await isVInstalled()
if (!isInstalled) {
throw new Error("V must be installed to build VLS.")
}
let buildPath
if (version) {
log(`Detected V version ${version}.`)
}
let buildPath: string
try {
log("Building VLS...")
window.showInformationMessage("Building VLS...")
if (vlsConfig().get<string>("buildPath") !== "") {
buildPath = vlsConfig().get<string>("buildPath")
const configuredPath = vlsConfig().get<string>("buildPath")?.trim() ?? ""
if (configuredPath !== "") {
buildPath = path.resolve(configuredPath)
try {
await fs.access(buildPath)
} catch {
throw new Error(`Configured VLS build path not found: ${buildPath}`)
}
} else {
// Use temporary directory for cross-platform compatibility
buildPath = path.join(os.tmpdir(), "vls")
// Remove any existing directory at buildPath
await fs.rm(buildPath, { recursive: true, force: true })
// Clone the repo into buildPath
await exec(`git clone --depth 1 https://github.com/vlang/vls.git ${buildPath}`)
await exec(`git clone --depth 1 https://github.com/vlang/vls.git "${buildPath}"`)
}
await execVInTerminalOnBG(["."], buildPath) // build

Expand Down
93 changes: 1 addition & 92 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
import { exec as _exec } from "child_process"
import { getVExecCommand } from "exec"
import * as fs from "fs"
import { USER_BIN_PATH } from "langserver"
import { log } from "logger"
import * as os from "os"
import * as path from "path"
import { promisify } from "util"
import { ProgressLocation, Uri, window, workspace, WorkspaceFolder } from "vscode"
import { Uri, window, workspace, WorkspaceFolder } from "vscode"

export const config = () => workspace.getConfiguration("v")

export const vlsConfig = () => workspace.getConfiguration("v.vls")

const exec = promisify(_exec)

/** Get current working directory.
* @param uri The URI of document
Expand All @@ -34,85 +25,3 @@ export function getWorkspaceFolder(uri?: Uri): WorkspaceFolder {
return workspace.workspaceFolders[0]
}
}

/**
* Checks if the 'v' command is available in the system's PATH.
* @returns A promise that resolves to true if 'v' is installed, otherwise false.
*/
export async function isVInstalled(): Promise<boolean> {
const vexec = getVExecCommand()
try {
// A simple command to check if V is installed and in the PATH.
const version = await exec(`${vexec} --version`)
log(`V is already installed, version: ${version.stdout.trim()}`)
return true
} catch (error) {
log(`V is not detected in PATH: ${error}`)
return false
}
}

/**
* Clone and build the `v` compiler
*
* Returns: absolute path to the `v` binary (string)
* Error: rejects if any git/make step fails
*/
export async function installV(): Promise<void> {
const installDir = USER_BIN_PATH
const vRepoPath = path.join(installDir, "v")
const repoUrl = "https://github.com/vlang/v"

await window.withProgress(
{
location: ProgressLocation.Notification,
title: "Installing V Language",
cancellable: false,
},
async (progress) => {
try {
// 0. Clean up any previous failed attempts
progress.report({ message: "Preparing workspace..." })
if (fs.existsSync(installDir)) {
fs.rmSync(installDir, { recursive: true, force: true })
}
fs.mkdirSync(installDir)

// 1. Clone the repository
progress.report({ message: "Cloning V repository..." })
await exec(`git clone --depth=1 ${repoUrl}`, { cwd: installDir })

// 2. Build V using make
progress.report({ message: "Building V from source (this may take a moment)..." })
await exec("make", { cwd: vRepoPath })

// 3. Create a symlink
// This command often requires sudo/admin privileges.
// We run it and inform the user to run it manually if it fails.
progress.report({ message: "Attempting to create symlink..." })

try {
// On Windows, the build script handles the path. On Linux/macOS, symlink is used.
const symlinkCommand =
os.platform() === "win32" ? "v.exe symlink" : "./v symlink"
await exec(symlinkCommand, { cwd: vRepoPath })

window.showInformationMessage(
"V language installed and linked successfully! Please restart VS Code to use the `v` command.",
)
} catch (symlinkError) {
console.error(symlinkError)
window.showWarningMessage(
`V was built successfully, but the automatic symlink failed (likely due to permissions). Please run '${path.join(vRepoPath, "v")} symlink' manually with administrator/sudo rights.`,
"OK",
)
}
} catch (error) {
console.error(error)
window.showErrorMessage(
`Failed to install V. Please check the logs for details. Error: ${error}`,
)
}
},
)
}
Loading
Loading