Integrate RustyLR LSP into the rustylr executable and publish VSCode extension support#82
Conversation
There was a problem hiding this comment.
Code Review
This pull request adds LSP-based editor support for RustyLR in VSCode, renaming the extension to rustylr-lsp and introducing version compatibility checks between the extension and the language server. Feedback on these changes suggests improving the version check command resolution to preserve custom wrapper arguments, increasing the execution timeout to prevent failures during slow cargo run compilations, and wrapping the execution in a try-catch block to handle failures gracefully. Additionally, the release script should be updated to keep package-lock.json in sync with package.json during version bumps.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| function resolveVersionCommand(command, args) { | ||
| const commandName = path.basename(command).toLowerCase(); | ||
| if (commandName === "cargo" || commandName === "cargo.exe") { | ||
| const separatorIndex = args.indexOf("--"); | ||
| if (separatorIndex >= 0) { | ||
| return { | ||
| command, | ||
| args: [...args.slice(0, separatorIndex + 1), "--version"] | ||
| }; | ||
| } | ||
| return { | ||
| command, | ||
| args: [...args, "--", "--version"] | ||
| }; | ||
| } | ||
| return { command, args: ["--version"] }; | ||
| } |
There was a problem hiding this comment.
The current implementation of resolveVersionCommand discards all arguments for non-cargo commands and replaces them with ["--version"]. This breaks custom wrapper commands (e.g., direnv exec . rustylr lsp or rustup run stable rustylr lsp), as it will execute the wrapper with --version instead of forwarding it to rustylr. Replacing the "lsp" argument with "--version" is a much more robust approach that preserves other wrapper arguments.
function resolveVersionCommand(command, args) {
const commandName = path.basename(command).toLowerCase();
if (commandName === "cargo" || commandName === "cargo.exe") {
const separatorIndex = args.indexOf("--");
if (separatorIndex >= 0) {
return {
command,
args: [...args.slice(0, separatorIndex + 1), "--version"]
};
}
return {
command,
args: [...args, "--", "--version"]
};
}
const lspIndex = args.indexOf("lsp");
if (lspIndex >= 0) {
const newArgs = [...args];
newArgs[lspIndex] = "--version";
return { command, args: newArgs };
}
return { command, args: [...args, "--version"] };
}| args, | ||
| { | ||
| cwd, | ||
| timeout: 10000, |
There was a problem hiding this comment.
A timeout of 10 seconds (10000 ms) is too short when running the server via cargo run (which is the default fallback for workspace contributors). Compiling the Rust project on the first run or after changes can easily exceed 10 seconds, causing a timeout error and preventing the extension from starting. Increasing this to 60 seconds is much safer.
| timeout: 10000, | |
| timeout: 60000, |
| args, | ||
| { | ||
| cwd, | ||
| timeout: 1e4, |
There was a problem hiding this comment.
A timeout of 10 seconds (1e4 ms) is too short when running the server via cargo run (which is the default fallback for workspace contributors). Compiling the Rust project on the first run or after changes can easily exceed 10 seconds, causing a timeout error and preventing the extension from starting. Increasing this to 60 seconds is much safer.
| timeout: 1e4, | |
| timeout: 6e4, |
| const versionCommand = server.versionCommand || resolveVersionCommand(server.command, server.args); | ||
| const output = await execFileText(versionCommand.command, versionCommand.args, cwd); | ||
| const actualVersion = parseRustylrVersion(output); | ||
| if (!actualVersion) { | ||
| throw new Error( | ||
| `Could not parse RustyLR language server version from '${versionCommand.command} ${versionCommand.args.join(" ")}'. Output: ${output.trim()}` | ||
| ); | ||
| } |
There was a problem hiding this comment.
If the version command fails to execute (e.g., rustylr is not installed or not on the PATH, or there is a timeout), execFileText will throw an error. Wrapping this call in a try-catch block allows us to log the failure and show a helpful error message to the user with an option to copy the install command, rather than letting the extension fail silently or cryptically.
const versionCommand = server.versionCommand || resolveVersionCommand(server.command, server.args);
let output;
try {
output = await execFileText(versionCommand.command, versionCommand.args, cwd);
} catch (error) {
outputChannel.appendLine(`Failed to execute version command: ${error.message}`);
const installCommand = `cargo install rustylr --version ${expectedVersion} --force`;
vscode.window.showErrorMessage(
`Failed to execute RustyLR language server version command. Please ensure 'rustylr' is installed and available on your PATH.`,
"Copy Install Command"
).then((selection) => {
if (selection === "Copy Install Command") {
vscode.env.clipboard.writeText(installCommand);
vscode.window.showInformationMessage(`Copied: ${installCommand}`);
}
});
throw error;
}
const actualVersion = parseRustylrVersion(output);
if (!actualVersion) {
throw new Error(
`Could not parse RustyLR language server version from '${versionCommand.command} ${versionCommand.args.join(" ")}'. Output: ${output.trim()}`
);
}| const versionCommand = server.versionCommand || resolveVersionCommand(server.command, server.args); | ||
| const output = await execFileText(versionCommand.command, versionCommand.args, cwd); | ||
| const actualVersion = parseRustylrVersion(output); | ||
| if (!actualVersion) { | ||
| throw new Error( | ||
| `Could not parse RustyLR language server version from '${versionCommand.command} ${versionCommand.args.join(" ")}'. Output: ${output.trim()}` | ||
| ); | ||
| } |
There was a problem hiding this comment.
If the version command fails to execute (e.g., rustylr is not installed or not on the PATH, or there is a timeout), execFileText will throw an error. Wrapping this call in a try-catch block allows us to log the failure and show a helpful error message to the user with an option to copy the install command, rather than letting the extension fail silently or cryptically.
const versionCommand = server.versionCommand || resolveVersionCommand(server.command, server.args);
let output;
try {
output = await execFileText(versionCommand.command, versionCommand.args, cwd);
} catch (error) {
outputChannel.appendLine(`Failed to execute version command: ${error.message}`);
const installCommand = `cargo install rustylr --version ${expectedVersion} --force`;
vscode.window.showErrorMessage(
`Failed to execute RustyLR language server version command. Please ensure 'rustylr' is installed and available on your PATH.`,
"Copy Install Command"
).then((selection) => {
if (selection === "Copy Install Command") {
vscode.env.clipboard.writeText(installCommand);
vscode.window.showInformationMessage(`Copied: ${installCommand}`);
}
});
throw error;
}
const actualVersion = parseRustylrVersion(output);
if (!actualVersion) {
throw new Error(
`Could not parse RustyLR language server version from '${versionCommand.command} ${versionCommand.args.join(" ")}'. Output: ${output.trim()}`
);
}| def update_vscode_extension_versions(root_dir, old_version, new_version): | ||
| """Update the VSCode extension version, required rustylr server version, and matching docs.""" | ||
| package_json_path = os.path.join(root_dir, 'editors', 'vscode-rustylr', 'package.json') | ||
|
|
||
| if os.path.exists(package_json_path): | ||
| with open(package_json_path, 'r', encoding='utf-8') as f: | ||
| package_content = f.read() | ||
|
|
||
| updated_package = False | ||
| version_pattern = r'("version"\s*:\s*")[^"]+(")' | ||
| if re.search(version_pattern, package_content): | ||
| package_content = re.sub(version_pattern, rf'\g<1>{new_version}\g<2>', package_content, count=1) | ||
| updated_package = True | ||
| print( | ||
| f"Updated VSCode extension package version in " | ||
| f"{os.path.relpath(package_json_path, root_dir)}: {old_version} -> {new_version}" | ||
| ) | ||
| else: | ||
| print( | ||
| f"Warning: Could not find extension version in " | ||
| f"{os.path.relpath(package_json_path, root_dir)}" | ||
| ) | ||
|
|
||
| server_pattern = r'("requiredServerVersion"\s*:\s*")[^"]+(")' | ||
| if re.search(server_pattern, package_content): | ||
| package_content = re.sub(server_pattern, rf'\g<1>{new_version}\g<2>', package_content) | ||
| updated_package = True | ||
| print( | ||
| f"Updated VSCode extension requiredServerVersion in " | ||
| f"{os.path.relpath(package_json_path, root_dir)}: {old_version} -> {new_version}" | ||
| ) | ||
| else: | ||
| print( | ||
| f"Warning: Could not find requiredServerVersion in " | ||
| f"{os.path.relpath(package_json_path, root_dir)}" | ||
| ) | ||
|
|
||
| if updated_package: | ||
| with open(package_json_path, 'w', encoding='utf-8') as f: | ||
| f.write(package_content) | ||
| else: | ||
| print(f"Warning: {package_json_path} does not exist.") |
There was a problem hiding this comment.
The update_vscode_extension_versions function updates package.json but leaves package-lock.json out of sync. To keep the package lockfile synchronized, we should also update the version fields in package-lock.json (specifically the root version and the empty package version) during release version bumps.
def update_vscode_extension_versions(root_dir, old_version, new_version):
"""Update the VSCode extension version, required rustylr server version, and matching docs."""
package_json_path = os.path.join(root_dir, 'editors', 'vscode-rustylr', 'package.json')
package_lock_path = os.path.join(root_dir, 'editors', 'vscode-rustylr', 'package-lock.json')
if os.path.exists(package_json_path):
with open(package_json_path, 'r', encoding='utf-8') as f:
package_content = f.read()
updated_package = False
version_pattern = r'("version"\s*:\s*")[^"]+(")'
if re.search(version_pattern, package_content):
package_content = re.sub(version_pattern, rf'\g<1>{new_version}\g<2>', package_content, count=1)
updated_package = True
print(
f"Updated VSCode extension package version in "
f"{os.path.relpath(package_json_path, root_dir)}: {old_version} -> {new_version}"
)
else:
print(
f"Warning: Could not find extension version in "
f"{os.path.relpath(package_json_path, root_dir)}"
)
server_pattern = r'("requiredServerVersion"\s*:\s*")[^"]+(")'
if re.search(server_pattern, package_content):
package_content = re.sub(server_pattern, rf'\g<1>{new_version}\g<2>', package_content)
updated_package = True
print(
f"Updated VSCode extension requiredServerVersion in "
f"{os.path.relpath(package_json_path, root_dir)}: {old_version} -> {new_version}"
)
else:
print(
f"Warning: Could not find requiredServerVersion in "
f"{os.path.relpath(package_json_path, root_dir)}"
)
if updated_package:
with open(package_json_path, 'w', encoding='utf-8') as f:
f.write(package_content)
if os.path.exists(package_lock_path):
import json
try:
with open(package_lock_path, 'r', encoding='utf-8') as f:
lock_data = json.load(f)
if 'version' in lock_data:
lock_data['version'] = new_version
if 'packages' in lock_data and '' in lock_data['packages'] and 'version' in lock_data['packages']['']:
lock_data['packages']['']['version'] = new_version
with open(package_lock_path, 'w', encoding='utf-8') as f:
json.dump(lock_data, f, indent=2)
f.write('\n')
print(
f"Updated VSCode extension package-lock version in "
f"{os.path.relpath(package_lock_path, root_dir)}: {old_version} -> {new_version}"
)
except Exception as e:
print(f"Warning: Failed to update package-lock.json: {e}")
else:
print(f"Warning: {package_json_path} does not exist.")
Summary
rustylrexecutable as therustylr lspsubcommand.rusty_lr_lspworkspace crate.rustylr lspand verify the installed server version before startup.RustyLR LSP.scripts/release.py.Details
rustylr.server.commandtarget/debug/rustylrortarget/release/rustylrin a RustyLR checkoutrustylronPATHcargo run --quiet --package rustylr -- lspinside a RustyLR checkoutrustylr lsp --stdiois accepted for clients that append an explicit stdio transport argument.rustylr --versionagainst its required server version and suggests a compatible install command if needed.https://marketplace.visualstudio.com/items?itemName=ehwan.rustylr-lsp
MIT OR Apache-2.0.