blob: 2ad57a0d60e13f492572f003daa0025b34b01863 [file] [log] [blame]
// Copyright 2017 The Rust Project Developers. See the COPYRIGHT
// file at the top-level directory of this distribution and at
// http://rust-lang.org/COPYRIGHT.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//! This module presents the RLS as a command line interface, it takes simple
//! versions of commands, turns them into messages the RLS will understand, runs
//! the RLS as usual and prints the JSON result back on the command line.
use actions::requests;
use analysis::{AnalysisHost, Target};
use config::Config;
use server::{self, LsService, NoParams, Notification, Request};
use vfs::Vfs;
use ls_types::{ClientCapabilities, DocumentFormattingParams, DocumentRangeFormattingParams,
FormattingOptions, InitializeParams, Position, Range, RenameParams,
TextDocumentIdentifier, TextDocumentPositionParams, TraceOption,
WorkspaceSymbolParams};
use std::collections::HashMap;
use std::fmt;
use std::io::{stdin, stdout, Write};
use std::marker::PhantomData;
use std::path::Path;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use std::sync::mpsc::{channel, Receiver, Sender};
use std::thread;
use std::time::{Duration, Instant};
use url::Url;
const VERBOSE: bool = false;
macro_rules! print_verb {
($($arg:tt)*) => {
if VERBOSE {
println!($($arg)*);
}
}
}
/// Run the RLS in command line mode.
pub fn run() {
let sender = init();
loop {
// Present a prompt and read from stdin.
print!("> ");
stdout().flush().unwrap();
let mut input = String::new();
stdin()
.read_line(&mut input)
.expect("Could not read from stdin");
// Split the input into an action command and args
let mut bits = input.split_whitespace();
let action = bits.next();
let action = match action {
Some(a) => a,
None => continue,
};
// Switch on the action and build an appropriate message.
let msg = match action {
"def" => {
let file_name = bits.next().expect("Expected file name");
let row = bits.next().expect("Expected line number");
let col = bits.next().expect("Expected column number");
def(file_name, row, col).to_string()
}
"rename" => {
let file_name = bits.next().expect("Expected file name");
let row = bits.next().expect("Expected line number");
let col = bits.next().expect("Expected column number");
let new_name = bits.next().expect("Expected new name");
rename(file_name, row, col, new_name).to_string()
}
"hover" => {
let file_name = bits.next().expect("Expected file name");
let row = bits.next().expect("Expected line number");
let col = bits.next().expect("Expected column number");
hover(file_name, row, col).to_string()
}
"symbol" => {
let query = bits.next().expect("Expected a query");
workspace_symbol(query).to_string()
}
"format" => {
let file_name = bits.next().expect("Expected file name");
let tab_size: u64 = bits.next()
.unwrap_or("4")
.parse()
.expect("Tab size should be an unsigned integer");
let insert_spaces: bool = bits.next()
.unwrap_or("true")
.parse()
.expect("Insert spaces should be 'true' or 'false'");
format(file_name, tab_size, insert_spaces).to_string()
}
"range_format" => {
let file_name = bits.next().expect("Expected file name");
let start_row: u64 = bits.next()
.expect("Expected start line")
.parse()
.expect("Bad start line");
let start_col: u64 = bits.next()
.expect("Expected start column")
.parse()
.expect("Bad start column");
let end_row: u64 = bits.next()
.expect("Expected end line")
.parse()
.expect("Bad end line");
let end_col: u64 = bits.next()
.expect("Expected end column")
.parse()
.expect("Bad end column");
let tab_size: u64 = bits.next()
.unwrap_or("4")
.parse()
.expect("Tab size should be an unsigned integer");
let insert_spaces: bool = bits.next()
.unwrap_or("true")
.parse()
.expect("Insert spaces should be 'true' or 'false'");
range_format(
file_name,
start_row,
start_col,
end_row,
end_col,
tab_size,
insert_spaces,
).to_string()
}
"h" | "help" => {
help();
continue;
}
"q" | "quit" => {
sender
.send(shutdown().to_string())
.expect("Error sending on channel");
sender
.send(exit().to_string())
.expect("Error sending on channel");
// Sometimes we don't quite exit in time and we get an error on the channel. Hack it.
thread::sleep(Duration::from_millis(100));
return;
}
_ => {
println!("Unknown action. Type 'help' to see available actions.");
continue;
}
};
// Send the message to the server.
print_verb!("message: {:?}", msg);
sender.send(msg).expect("Error sending on channel");
// Give the result time to print before printing the prompt again.
thread::sleep(Duration::from_millis(100));
}
}
fn def(file_name: &str, row: &str, col: &str) -> Request<requests::Definition> {
let params = TextDocumentPositionParams {
text_document: TextDocumentIdentifier::new(url(file_name)),
position: Position::new(
u64::from_str(row).expect("Bad line number"),
u64::from_str(col).expect("Bad column number"),
),
};
Request {
id: next_id(),
params,
received: Instant::now(),
_action: PhantomData,
}
}
fn rename(file_name: &str, row: &str, col: &str, new_name: &str) -> Request<requests::Rename> {
let params = RenameParams {
text_document: TextDocumentIdentifier::new(url(file_name)),
position: Position::new(
u64::from_str(row).expect("Bad line number"),
u64::from_str(col).expect("Bad column number"),
),
new_name: new_name.to_owned(),
};
Request {
id: next_id(),
params,
received: Instant::now(),
_action: PhantomData,
}
}
fn hover(file_name: &str, row: &str, col: &str) -> Request<requests::Hover> {
let params = TextDocumentPositionParams {
text_document: TextDocumentIdentifier::new(url(file_name)),
position: Position::new(
u64::from_str(row).expect("Bad line number"),
u64::from_str(col).expect("Bad column number"),
),
};
Request {
id: next_id(),
params,
received: Instant::now(),
_action: PhantomData,
}
}
fn workspace_symbol(query: &str) -> Request<requests::WorkspaceSymbol> {
let params = WorkspaceSymbolParams {
query: query.to_owned(),
};
Request {
id: next_id(),
params,
received: Instant::now(),
_action: PhantomData,
}
}
fn format(file_name: &str, tab_size: u64, insert_spaces: bool) -> Request<requests::Formatting> {
// no optional properties
let properties = HashMap::default();
let params = DocumentFormattingParams {
text_document: TextDocumentIdentifier::new(url(file_name)),
options: FormattingOptions {
tab_size,
insert_spaces,
properties,
},
};
Request {
id: next_id(),
params,
received: Instant::now(),
_action: PhantomData,
}
}
fn range_format(
file_name: &str,
start_row: u64,
start_col: u64,
end_row: u64,
end_col: u64,
tab_size: u64,
insert_spaces: bool,
) -> Request<requests::RangeFormatting> {
// no optional properties
let properties = HashMap::default();
let params = DocumentRangeFormattingParams {
text_document: TextDocumentIdentifier::new(url(file_name)),
range: Range {
start: Position::new(start_row, start_col),
end: Position::new(end_row, end_col),
},
options: FormattingOptions {
tab_size,
insert_spaces,
properties,
},
};
Request {
id: next_id(),
params,
received: Instant::now(),
_action: PhantomData,
}
}
fn shutdown<'a>() -> Request<server::ShutdownRequest<'a>> {
Request {
id: next_id(),
params: NoParams {},
received: Instant::now(),
_action: PhantomData,
}
}
fn exit<'a>() -> Notification<server::ExitNotification<'a>> {
Notification {
params: NoParams {},
_action: PhantomData,
}
}
fn initialize(root_path: String) -> Request<server::InitializeRequest> {
let params = InitializeParams {
process_id: None,
root_path: Some(root_path), // FIXME(#299): This property is deprecated. Instead Use `root_uri`.
root_uri: None,
initialization_options: None,
capabilities: ClientCapabilities {
workspace: None,
text_document: None,
experimental: None,
},
trace: TraceOption::Off,
};
Request {
id: next_id(),
params,
received: Instant::now(),
_action: PhantomData,
}
}
fn url(file_name: &str) -> Url {
let path = Path::new(file_name)
.canonicalize()
.expect("Could not canonicalize file name");
Url::parse(&format!("file://{}", path.to_str().unwrap())).expect("Bad file name")
}
fn next_id() -> usize {
static mut ID: usize = 0;
unsafe {
ID += 1;
ID
}
}
// Custom reader and output for the RLS server.
#[derive(Clone)]
struct PrintlnOutput;
impl server::Output for PrintlnOutput {
fn response(&self, output: String) {
println!("{}", output);
}
fn provide_id(&self) -> u32 {
0
}
fn success<D: ::serde::Serialize + fmt::Debug>(&self, id: usize, data: &D) {
println!("{}: {:#?}", id, data);
}
}
struct ChannelMsgReader {
channel: Mutex<Receiver<String>>,
}
impl ChannelMsgReader {
fn new(rx: Receiver<String>) -> ChannelMsgReader {
ChannelMsgReader {
channel: Mutex::new(rx),
}
}
}
impl server::MessageReader for ChannelMsgReader {
fn read_message(&self) -> Option<String> {
let channel = self.channel.lock().unwrap();
let msg = channel.recv().expect("Error reading from channel");
Some(msg)
}
}
// Initialize a server, returns the sender end of a channel for posting messages.
// The initialized server will live on its own thread and look after the receiver.
fn init() -> Sender<String> {
let analysis = Arc::new(AnalysisHost::new(Target::Debug));
let vfs = Arc::new(Vfs::new());
let (sender, receiver) = channel();
let service = LsService::new(
analysis,
vfs,
Arc::new(Mutex::new(Config::default())),
Box::new(ChannelMsgReader::new(receiver)),
PrintlnOutput,
);
thread::spawn(move || LsService::run(service));
sender
.send(
initialize(
::std::env::current_dir()
.unwrap()
.to_str()
.unwrap()
.to_owned(),
).to_string(),
)
.expect("Error sending init");
println!("Initializing (look for `diagnosticsEnd` message)...");
sender
}
// Display help message.
fn help() {
println!("RLS command line interface.");
println!("\nLine and column numbers are zero indexed");
println!("\nSupported commands:");
println!(" help display this message");
println!(" quit exit");
println!("");
println!(" def file_name line_number column_number");
println!(" textDocument/definition");
println!(" used for 'goto def'");
println!("");
println!(" rename file_name line_number column_number new_name");
println!(" textDocument/rename");
println!(" used for 'rename'");
println!("");
println!(" hover file_name line_number column_number");
println!(" textDocument/hover");
println!(" used for 'hover'");
println!("");
println!(" symbol query");
println!(" workspace/symbol");
println!("");
println!(" format file_name [tab_size [insert_spaces]]");
println!(" textDocument/formatting");
println!(" tab_size defaults to 4 and insert_spaces to 'true'");
println!("");
println!(" range_format file_name start_line start_col end_line end_col [tab_size [insert_spaces]]");
println!(" textDocument/rangeFormatting");
println!(" tab_size defaults to 4 and insert_spaces to 'true'");
}