Skip to content

Integrate RustyLR LSP into the rustylr executable and publish VSCode extension support#82

Merged
ehwan merged 9 commits into
mainfrom
vscode_extension
Jun 23, 2026
Merged

Integrate RustyLR LSP into the rustylr executable and publish VSCode extension support#82
ehwan merged 9 commits into
mainfrom
vscode_extension

Conversation

@ehwan

@ehwan ehwan commented Jun 23, 2026

Copy link
Copy Markdown
Owner

Summary

  • Move the RustyLR LSP server into the rustylr executable as the rustylr lsp subcommand.
  • Remove the standalone rusty_lr_lsp workspace crate.
  • Update the VSCode extension to launch rustylr lsp and verify the installed server version before startup.
  • Rename and prepare the VSCode extension as RustyLR LSP.
  • Add Marketplace installation documentation and LSP usage guidance.
  • Keep VSCode extension version metadata synchronized through scripts/release.py.

Details

  • The VSCode extension now resolves the language server in this order:
    • configured rustylr.server.command
    • local target/debug/rustylr or target/release/rustylr in a RustyLR checkout
    • rustylr on PATH
    • cargo run --quiet --package rustylr -- lsp inside a RustyLR checkout
  • rustylr lsp --stdio is accepted for clients that append an explicit stdio transport argument.
  • The extension checks rustylr --version against its required server version and suggests a compatible install command if needed.
  • Documentation now links to the published Marketplace extension:
    https://marketplace.visualstudio.com/items?itemName=ehwan.rustylr-lsp
  • The extension package uses the same dual license as RustyLR: MIT OR Apache-2.0.

@ehwan ehwan self-assigned this Jun 23, 2026
@ehwan ehwan merged commit 71f0f11 into main Jun 23, 2026
1 check passed
@ehwan ehwan deleted the vscode_extension branch June 23, 2026 04:10

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread editors/vscode-rustylr/extension.js
Comment on lines +22868 to +22884
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"] };
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
timeout: 10000,
timeout: 60000,

args,
{
cwd,
timeout: 1e4,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
timeout: 1e4,
timeout: 6e4,

Comment on lines +237 to +244
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()}`
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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()}`
    );
  }

Comment on lines +22890 to +22897
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()}`
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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()}`
    );
  }

Comment thread scripts/release.py
Comment on lines +91 to +132
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.")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant