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 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
113 changes: 34 additions & 79 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,13 @@

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;
import software.amazon.smithy.cli.AnsiColorFormatter;
import software.amazon.smithy.cli.CliPrinter;
import software.amazon.smithy.cli.HelpPrinter;

/**
* Main launcher for the Language server, started by the editor.
Expand All @@ -32,90 +31,46 @@ 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()) {
printHelp(serverArguments);
System.exit(0);
}

launch(serverArguments);
}

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

/**
* @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;
private static void startServer(InputStream in, OutputStream out) throws Exception {
var server = new SmithyLanguageServer();
var launcher = LSPLauncher.createServerLauncher(server, in, 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();
}

Optional<Exception> launchFailure = launch(in, out);
var client = launcher.getRemoteProxy();
server.connect(client);

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);
launcher.startListening().get();
}

e.printStackTrace();
} finally {
try {
if (socket != null) {
socket.close();
}
} catch (Exception e) {
System.out.println("Failed to close the socket");
System.out.println(e);
}
}
private static void printHelp(ServerArguments serverArguments) {
CliPrinter printer = CliPrinter.fromOutputStream(System.out);
HelpPrinter helpPrinter = new HelpPrinter("smithy-language-server");
serverArguments.registerHelp(helpPrinter);
helpPrinter.summary("Run the Smithy Language Server.");
helpPrinter.print(AnsiColorFormatter.AUTO, printer);
printer.flush();
}
}
101 changes: 101 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,101 @@
/*
* 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.ArgumentReceiver;
import software.amazon.smithy.cli.Arguments;
import software.amazon.smithy.cli.CliError;
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 (!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;
}

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

private int validatePortNumber(String portStr) {
try {
int portNumber = Integer.parseInt(portStr);
if (portNumber < MIN_PORT || portNumber > MAX_PORT) {
throw invalidPort(portStr);
} else {
return portNumber;
}
} catch (NumberFormatException e) {
throw invalidPort(portStr);
}
}

private static CliError invalidPort(String portStr) {
return new CliError("Invalid port number: expected an integer between "
+ MIN_PORT + " and " + MAX_PORT + ", inclusive. Was: " + portStr);
}
}
111 changes: 111 additions & 0 deletions src/test/java/software/amazon/smithy/lsp/ServerArgumentsTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
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.assertFalse;
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());
assertFalse(serverArguments.help());
assertTrue(serverArguments.useSocket());
}

@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());
assertFalse(serverArguments.help());
assertTrue(serverArguments.useSocket());
}

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

assertEquals(0, serverArguments.port());
assertFalse(serverArguments.help());
assertFalse(serverArguments.useSocket());
}

@Test
void defaultPortNumberInArg() {
String[] args = {"0"};
ServerArguments serverArguments = ServerArguments.create(args);
assertEquals(0, serverArguments.port());
assertFalse(serverArguments.help());
assertFalse(serverArguments.useSocket());
}

@Test
void defaultPortNumberWithFlag() {
String[] args = {"--port","0"};
ServerArguments serverArguments = ServerArguments.create(args);
assertEquals(0, serverArguments.port());
assertFalse(serverArguments.help());
assertFalse(serverArguments.useSocket());
}

@Test
void defaultPortNumberWithShotFlag() {
String[] args = {"-p","0"};
ServerArguments serverArguments = ServerArguments.create(args);
assertEquals(0, serverArguments.port());
assertFalse(serverArguments.help());
assertFalse(serverArguments.useSocket());
}

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

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

@Test
void validHelpShort() {
String[] args = {"-h"};
ServerArguments serverArguments = ServerArguments.create(args);
assertTrue(serverArguments.help());
assertFalse(serverArguments.useSocket());
}

@Test
void validHelp() {
String[] args = {"--help"};
ServerArguments serverArguments = ServerArguments.create(args);
assertTrue(serverArguments.help());
assertFalse(serverArguments.useSocket());
}
}