Skip to content

Add argument parser feature to the LSP. #218

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 14, 2025
Merged
Show file tree
Hide file tree
Changes from 4 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
102 changes: 22 additions & 80 deletions src/main/java/software/amazon/smithy/lsp/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,10 @@

package software.amazon.smithy.lsp;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Optional;
import org.eclipse.lsp4j.jsonrpc.Launcher;
import org.eclipse.lsp4j.launch.LSPLauncher;
import org.eclipse.lsp4j.services.LanguageClient;

/**
* Main launcher for the Language server, started by the editor.
Expand All @@ -32,90 +28,36 @@ private Main() {
}

/**
* Launch the LSP and wait for it to terminate.
*
* @param in input stream for communication
* @param out output stream for communication
* @return Empty Optional if service terminated successfully, error otherwise
* Main entry point for the language server.
* @param args Arguments passed to the server.
* @throws Exception If there is an error starting the server.
*/
public static Optional<Exception> launch(InputStream in, OutputStream out) {
SmithyLanguageServer server = new SmithyLanguageServer();
Launcher<LanguageClient> launcher = LSPLauncher.createServerLauncher(
server,
exitOnClose(in),
out);

LanguageClient client = launcher.getRemoteProxy();

server.connect(client);
try {
launcher.startListening().get();
return Optional.empty();
} catch (Exception e) {
return Optional.of(e);
public static void main(String[] args) throws Exception {
var serverArguments = ServerArguments.create(args);
if (serverArguments.help()) {
System.exit(0);
}
}

private static InputStream exitOnClose(InputStream delegate) {
return new InputStream() {
@Override
public int read() throws IOException {
int result = delegate.read();
if (result < 0) {
System.exit(0);
}
return result;
}
};
launch(serverArguments);
}

/**
* @param args Arguments passed to launch server. First argument must either be
* a port number for socket connection, or 0 to use STDIN and STDOUT
* for communication
*/
public static void main(String[] args) {

Socket socket = null;
InputStream in;
OutputStream out;

try {
String port = args[0];
// If port is set to "0", use System.in/System.out.
if (port.equals("0")) {
in = System.in;
out = System.out;
} else {
socket = new Socket("localhost", Integer.parseInt(port));
in = socket.getInputStream();
out = socket.getOutputStream();
private static void launch(ServerArguments serverArguments) throws Exception {
if (serverArguments.useSocket()) {
try (var socket = new Socket("localhost", serverArguments.port())) {
startServer(socket.getInputStream(), socket.getOutputStream());
}
} else {
startServer(System.in, System.out);
}
}

Optional<Exception> launchFailure = launch(in, out);
private static void startServer(InputStream in, OutputStream out) throws Exception {
var server = new SmithyLanguageServer();
var launcher = LSPLauncher.createServerLauncher(server, in, out);

if (launchFailure.isPresent()) {
throw launchFailure.get();
} else {
System.out.println("Server terminated without errors");
}
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Missing port argument");
} catch (NumberFormatException e) {
System.out.println("Port number must be a valid integer");
} catch (Exception e) {
System.out.println(e);
var client = launcher.getRemoteProxy();
server.connect(client);

e.printStackTrace();
} finally {
try {
if (socket != null) {
socket.close();
}
} catch (Exception e) {
System.out.println("Failed to close the socket");
System.out.println(e);
}
}
launcher.startListening().get();
}
}
111 changes: 111 additions & 0 deletions src/main/java/software/amazon/smithy/lsp/ServerArguments.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.lsp;

import java.util.function.Consumer;
import software.amazon.smithy.cli.AnsiColorFormatter;
import software.amazon.smithy.cli.ArgumentReceiver;
import software.amazon.smithy.cli.Arguments;
import software.amazon.smithy.cli.CliError;
import software.amazon.smithy.cli.CliPrinter;
import software.amazon.smithy.cli.HelpPrinter;

/**
* Options and Params available for LSP.
*/
final class ServerArguments implements ArgumentReceiver {

private static final int MIN_PORT = 0;
private static final int MAX_PORT = 65535;
private static final int DEFAULT_PORT = 0; // Default value for unset port number.
private static final String HELP = "--help";
private static final String HELP_SHORT = "-h";
private static final String PORT = "--port";
private static final String PORT_SHORT = "-p";
private static final String PORT_POSITIONAL = "<port>";
private int port = DEFAULT_PORT;
private boolean help = false;


static ServerArguments create(String[] args) {
Arguments arguments = Arguments.of(args);
var serverArguments = new ServerArguments();
arguments.addReceiver(serverArguments);
var positional = arguments.getPositional();
if (serverArguments.help()) {
serverArguments.printHelp(arguments);
}
if (!positional.isEmpty()) {
serverArguments.port = serverArguments.validatePortNumber(positional.getFirst());
}
return serverArguments;
}

@Override
public void registerHelp(HelpPrinter printer) {
printer.option(HELP, HELP_SHORT, "Print this help output.");
printer.param(PORT, PORT_SHORT, "PORT",
"The port to use for talking to the client. When not specified, or set to 0, "
+ "standard in/out is used. Standard in/out is preferred, "
+ "so usually this shouldn't be specified.");
printer.option(PORT_POSITIONAL, null, "Deprecated: use --port instead. When not specified, or set to 0, "
+ "standard in/out is used. Standard in/out is preferred, so usually this shouldn't be specified.");
}

@Override
public boolean testOption(String name) {
if (name.equals(HELP) || name.equals(HELP_SHORT)) {
help = true;
return true;
}
return false;
}

@Override
public Consumer<String> testParameter(String name) {
if (name.equals(PORT_SHORT) || name.equals(PORT)) {
return value -> {
port = validatePortNumber(value);
};
}
return null;
}

int port() {
return port;
}

boolean help() {
return help;
}

public boolean useSocket() {
return port != 0;
}

private int validatePortNumber(String portStr) {
try {
int portNumber = Integer.parseInt(portStr);
if (portNumber < MIN_PORT || portNumber > MAX_PORT) {
throw new CliError("Invalid port number: should be an integer between "
+ MIN_PORT + " and " + MAX_PORT + ", inclusive.");
} else {
return portNumber;
}
} catch (NumberFormatException e) {
throw new CliError("Invalid port number: Can not parse " + portStr);
}
}

private void printHelp(Arguments arguments) {
CliPrinter printer = CliPrinter.fromOutputStream(System.out);
HelpPrinter helpPrinter = HelpPrinter.fromArguments("smithy-language-server", arguments);
helpPrinter.summary("Run the Smithy Language Server.");
helpPrinter.print(AnsiColorFormatter.AUTO, printer);
printer.flush();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package software.amazon.smithy.lsp;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import org.junit.jupiter.api.Test;
import software.amazon.smithy.cli.CliError;


import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class ServerArgumentsTest {
@Test
void validPositionalPortNumber() {
String[] args = {"1"};
ServerArguments serverArguments = ServerArguments.create(args);
assertEquals(1, serverArguments.port());
}

@Test
void invalidPositionalPortNumber() {
String[] args = {"65536"};
assertThrows(CliError.class,()-> {ServerArguments.create(args);});
}

@Test
void invalidFlagPortNumber() {
String[] args = {"-p","65536"};
assertThrows(CliError.class,()-> {ServerArguments.create(args);});
}

@Test
void validFlagPortNumberShort() {
String[] args = {"-p","100"};
ServerArguments serverArguments = ServerArguments.create(args);
assertEquals(100, serverArguments.port());
}

@Test
void defaultPortNumber() {
String[] args = {};
ServerArguments serverArguments = ServerArguments.create(args);

assertEquals(0, serverArguments.port());
}

@Test
void defaultPortNumberInArg() {
String[] args = {"0"};
ServerArguments serverArguments = ServerArguments.create(args);

assertEquals(0, serverArguments.port());
}

@Test
void validFlagPortNumber() {
String[] args = {"--port","200"};
ServerArguments serverArguments = ServerArguments.create(args);
assertEquals(200, serverArguments.port());
}

@Test
void validHelp() {
ByteArrayOutputStream outContent = new ByteArrayOutputStream();
PrintStream originalOut = System.out;
System.setOut(new PrintStream(outContent));

try {
ServerArguments.create(new String[]{"--help"});

String output = outContent.toString().trim();

assertTrue(output.contains("--help"));
assertTrue(output.contains("-h"));
assertTrue(output.contains("--port"));
assertTrue(output.contains("-p"));
assertTrue(output.contains("PORT"));
assertTrue(output.contains("<port>"));

} finally {
// Restore original System.out
System.setOut(originalOut);
}
}

@Test
void invalidFlag() {
String[] args = {"--foo"};
assertThrows(CliError.class,()-> {ServerArguments.create(args);});
}
}