Skip to content
Merged
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
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@
{
"command": "neva.updateLanguageTools",
"title": "Neva: Update Language Tools"
},
{
"command": "neva.upgradeCli",
"title": "Neva: Upgrade CLI"
}
],
"languages": [
Expand Down
29 changes: 28 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const setTextualModeCommandId = "neva.openTextualMode";
const setVisualModeCommandId = "neva.openVisualMode";
const installLanguageToolsCommandId = "neva.installLanguageTools";
const updateLanguageToolsCommandId = "neva.updateLanguageTools";
const upgradeNevaCommandId = "neva.upgradeCli";
const mainDefRegex = /^[ \t]*(pub[ \t]+)?def[ \t]+Main\b/gm;
const nevaEditorModeContextKey = "neva.editorMode";
const nevaEditorContextKey = "neva.activeEditorIsNeva";
Expand Down Expand Up @@ -96,6 +97,27 @@ function installLanguageTools() {
);
}

function upgradeNeva() {
const terminal = window.createTerminal("Neva Upgrade");
terminal.show(true);
terminal.sendText("neva upgrade", true);
window.showInformationMessage(
"Neva upgrade was opened in a terminal. Restart VS Code after it completes."
);
}

async function showLegacyCliUpgradePrompt() {
const selection = await window.showWarningMessage(
"Your Neva CLI is older than 0.39.0. Language features remain available through the installed Neva Language Server, " +
"but Run requires a newer Neva CLI.",
"Upgrade Neva"
);

if (selection === "Upgrade Neva") {
upgradeNeva();
}
}

async function updateActiveEditorContext(editor: TextEditor | undefined) {
const isNeva = editor?.document.languageId === "neva";
await commands.executeCommand("setContext", nevaEditorContextKey, isNeva);
Expand Down Expand Up @@ -131,7 +153,9 @@ export async function activate(context: ExtensionContext) {
extensionContext = context;

// Run language server, initialize client and establish connection
lspClient = setupLsp(context, process.env.VSCODE_NEVA_DEBUG === "true");
lspClient = setupLsp(context, process.env.VSCODE_NEVA_DEBUG === "true", {
onLegacyCliFallback: () => void showLegacyCliUpgradePrompt(),
});
lspClient.onNotification("neva/analyzer_message", (message: string) => {
window.showWarningMessage(message);
});
Expand All @@ -143,6 +167,7 @@ export async function activate(context: ExtensionContext) {
commands.registerCommand(runMainCommandId, runNeva),
commands.registerCommand(installLanguageToolsCommandId, installLanguageTools),
commands.registerCommand(updateLanguageToolsCommandId, installLanguageTools),
commands.registerCommand(upgradeNevaCommandId, upgradeNeva),
commands.registerCommand(setTextualModeCommandId, () => setEditorMode("textual")),
commands.registerCommand(setVisualModeCommandId, () => setEditorMode("visual")),
commands.registerCommand("neva.getEditorMode", () => currentMode),
Expand All @@ -163,6 +188,8 @@ export async function activate(context: ExtensionContext) {

export function deactivate(): Thenable<void> | undefined {
onDidChangeEditorModeEmitter.dispose();
runTerminal?.dispose();
runTerminal = undefined;
return lspClient && lspClient.stop();
}

Expand Down
28 changes: 20 additions & 8 deletions src/lsp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ interface LspLaunchCommand {
usesLegacyCliFallback?: boolean;
}

interface LspEventHandlers {
onLegacyCliFallback?: () => void;
}

function configuredLspPath(): string | undefined {
const configuredPath = workspace
.getConfiguration("neva")
Expand All @@ -32,7 +36,14 @@ function configuredLspPath(): string | undefined {
}

function legacyNevaToolCli(): boolean {
const result = cp.spawnSync("neva", ["tool"], { encoding: "utf8" });
// Some legacy Neva CLIs do not report an unknown `tool` command; they wait
// for input instead. Never let that probe block extension activation.
const result = cp.spawnSync("neva", ["tool"], {
encoding: "utf8",
timeout: 1_000,
});
const error = result.error as NodeJS.ErrnoException | undefined;
if (error?.code === "ETIMEDOUT") return true;
if (result.error) return false;

return `${result.stdout ?? ""}${result.stderr ?? ""}`.includes("No help topic for 'tool'");
Expand Down Expand Up @@ -76,6 +87,8 @@ function resolveLspLaunchCommand(): LspLaunchCommand {
}

async function waitForProcessStart(process: cp.ChildProcessWithoutNullStreams): Promise<void> {
if (process.pid !== undefined) return;

await new Promise<void>((resolve, reject) => {
process.once("spawn", resolve);
process.once("error", reject);
Expand All @@ -95,12 +108,11 @@ function oldNevaToolMessage(): string {
"Update Neva with `neva upgrade`, restart VS Code, then run `Neva: Update Language Tools`.";
}

function legacyCliFallbackMessage(): string {
return "Neva Language Server started directly because your Neva CLI is older than 0.39.0. " +
"Language features are available, but Run requires updating Neva with `neva upgrade` and restarting VS Code.";
}

export function setupLsp(context: ExtensionContext, isDebug: boolean): LanguageClient {
export function setupLsp(
context: ExtensionContext,
isDebug: boolean,
handlers: LspEventHandlers = {}
): LanguageClient {
console.info("initializing lsp-client, extension mode: ", context.extensionMode);

let outputChannel: OutputChannel | undefined;
Expand Down Expand Up @@ -148,7 +160,7 @@ export function setupLsp(context: ExtensionContext, isDebug: boolean): LanguageC

if (command.usesLegacyCliFallback && !reportedLegacyCliFallback) {
reportedLegacyCliFallback = true;
window.showWarningMessage(legacyCliFallbackMessage());
handlers.onLegacyCliFallback?.();
}

const appendProcessOutput = (data: Buffer) => {
Expand Down
18 changes: 16 additions & 2 deletions test/integration/runTest.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
const path = require('path');
const fs = require('fs');
const process = require('process');
const { runTests } = require('@vscode/test-electron');
const { downloadAndUnzipVSCode, runTests } = require('@vscode/test-electron');

async function vscodeExecutablePath() {
const downloadedPath = await downloadAndUnzipVSCode({ version: 'stable' });

// Recent macOS VS Code archives use `Code`; @vscode/test-electron 2.5.2
// still resolves the historical `Electron` filename.
if (process.platform === 'darwin' && !fs.existsSync(downloadedPath)) {
const codePath = path.join(path.dirname(downloadedPath), 'Code');
if (fs.existsSync(codePath)) return codePath;
}

return downloadedPath;
}

async function main() {
try {
Expand All @@ -15,7 +29,7 @@ async function main() {
extensionDevelopmentPath,
extensionTestsPath,
launchArgs: [testWorkspacePath, '--disable-extensions'],
version: 'stable',
vscodeExecutablePath: await vscodeExecutablePath(),
});
} catch (error) {
console.error('Failed to run extension tests');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ suite('Neva Extension Host integration contract', () => {
'neva.openVisualMode',
'neva.installLanguageTools',
'neva.updateLanguageTools',
'neva.upgradeCli',
]) {
assert.ok(commands.includes(command), `Expected ${command} to be registered`);
}
Expand Down
Loading