blob: 292347cd3dc3adfffb2f97402d5eef0342d733c2 [file] [log] [blame]
//! 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 crate::actions::requests;
use crate::config::Config;
use crate::server::{self, LsService, Notification, Request, RequestId};
use rls_analysis::{AnalysisHost, Target};
use rls_vfs::Vfs;
use std::sync::atomic::{AtomicU64, Ordering};
use lsp_types::{
ClientCapabilities, CodeActionContext, CodeActionParams, CompletionItem,
DocumentFormattingParams, DocumentRangeFormattingParams, DocumentSymbolParams,
FormattingOptions, InitializeParams, Position, Range, RenameParams, TextDocumentIdentifier,
TextDocumentPositionParams, TraceOption, WindowClientCapabilities, 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::mpsc::{channel, Receiver, Sender};
use std::sync::{Arc, Mutex};
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)*);
}
}
}
/// Runs 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()
}
"document" => {
let query = bits.next().expect("Expected file name");
document_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()
}
"code_action" => {
let file_name = bits.next().expect("Expect file name");
let start_row: u64 =
bits.next().expect("Expect start line").parse().expect("Bad start line");
let start_col: u64 =
bits.next().expect("Expect start column").parse().expect("Bad start column");
let end_row: u64 =
bits.next().expect("Expect end line").parse().expect("Bad end line");
let end_col: u64 =
bits.next().expect("Expect end column").parse().expect("Bad end column");
code_action(file_name, start_row, start_col, end_row, end_col).to_string()
}
"resolve" => {
let label = bits.next().expect("Expect label");
let detail = bits.next().expect("Expect detail");
resolve_completion(label, detail).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 document_symbol(file_name: &str) -> Request<requests::Symbols> {
let params =
DocumentSymbolParams { text_document: TextDocumentIdentifier::new(url(file_name)) };
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 code_action(
file_name: &str,
start_row: u64,
start_col: u64,
end_row: u64,
end_col: u64,
) -> Request<requests::CodeAction> {
let params = CodeActionParams {
text_document: TextDocumentIdentifier::new(url(file_name)),
range: Range {
start: Position::new(start_row, start_col),
end: Position::new(end_row, end_col),
},
context: CodeActionContext { diagnostics: Vec::new(), only: None },
};
Request { id: next_id(), params, received: Instant::now(), _action: PhantomData }
}
fn resolve_completion(label: &str, detail: &str) -> Request<requests::ResolveCompletion> {
let params = CompletionItem::new_simple(label.to_owned(), detail.to_owned());
Request { id: next_id(), params, received: Instant::now(), _action: PhantomData }
}
fn shutdown() -> Request<server::ShutdownRequest> {
Request { id: next_id(), params: (), received: Instant::now(), _action: PhantomData }
}
fn exit() -> Notification<server::ExitNotification> {
Notification { params: (), _action: PhantomData }
}
fn initialize(root_path: String) -> Request<server::InitializeRequest> {
let params = InitializeParams {
process_id: None,
root_uri: Some(url(&root_path)),
root_path: Some(root_path),
initialization_options: None,
capabilities: ClientCapabilities {
workspace: None,
window: Some(WindowClientCapabilities { progress: Some(true) }),
text_document: None,
experimental: None,
},
trace: Some(TraceOption::Off),
workspace_folders: None,
};
Request { id: next_id(), params, received: Instant::now(), _action: PhantomData }
}
fn url(file_name: &str) -> Url {
let canonical = Path::new(file_name).canonicalize().expect("Could not canonicalize file name");
let mut path = canonical.to_str().unwrap();
// workaround for UNC path see https://github.com/rust-lang/rust/issues/42869
if path.starts_with(r"\\?\") {
path = &path[r"\\?\".len()..];
}
Url::parse(&format!("file://{}", path)).expect("Bad file name")
}
fn next_id() -> RequestId {
static ID: AtomicU64 = AtomicU64::new(1);
RequestId::Num(ID.fetch_add(1, Ordering::SeqCst))
}
// 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) -> RequestId {
RequestId::Num(0)
}
fn success<D: ::serde::Serialize + fmt::Debug>(&self, id: RequestId, 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,
// Don't clear `RUST_LOG` in CLI mode since it's intended for debugging purposes.
Arc::new(Mutex::new(Config { clear_env_rust_log: false, ..Default::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 `progress[done:true]` message)...");
sender
}
// Display help message.
fn help() {
println!(
"\
RLS command line interface.
Line and column numbers are zero indexed
Supported commands:
help display this message
quit exit
def file_name line_number column_number
textDocument/definition
used for 'goto def'
rename file_name line_number column_number new_name
textDocument/rename
used for 'rename'
hover file_name line_number column_number
textDocument/hover
used for 'hover'
symbol query
workspace/symbol
document file_name
textDocument/documentSymbol
format file_name [tab_size [insert_spaces]]
textDocument/formatting
tab_size defaults to 4 and insert_spaces to 'true'
range_format file_name start_line start_col end_line end_col [tab_size [insert_spaces]]
textDocument/rangeFormatting
tab_size defaults to 4 and insert_spaces to 'true'
code_action file_name start_line start_col end_line end_col
textDocument/codeAction
resolve label detail
completionItem/resolve"
);
}
// Reproducible on Windows where current dir can produce a url something like
// `file:////?\C:\Users\alex\project\rls` which will later cause issues
#[test]
fn url_workaround_unc_canonicals() {
let current_dir = ::std::env::current_dir().unwrap();
let url = url(current_dir.to_str().unwrap());
let url_str = format!("{}", url);
assert!(!url_str.starts_with(r"file:////?\"), "Unexpected UNC url {}", url);
}