From 120789804d5483f14c9682b2b777adf6d2992547 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Fri, 10 Aug 2018 21:13:39 +0300 Subject: Add line index --- codeless/server/src/caps.rs | 17 ++++++++-- codeless/server/src/dispatch.rs | 41 +++++++++++++++++++--- codeless/server/src/handlers.rs | 13 +++++++ codeless/server/src/main.rs | 75 ++++++++++++++++++++++++++++++++++------- codeless/server/src/req.rs | 25 +++++++++++--- codeless/src/extension.ts | 55 +++++++++++++++++++++++++++--- libanalysis/Cargo.toml | 1 + libanalysis/src/lib.rs | 14 +++++--- libeditor/Cargo.toml | 3 +- libeditor/src/lib.rs | 2 ++ libeditor/src/line_index.rs | 56 ++++++++++++++++++++++++++++++ 11 files changed, 269 insertions(+), 33 deletions(-) create mode 100644 codeless/server/src/handlers.rs create mode 100644 libeditor/src/line_index.rs diff --git a/codeless/server/src/caps.rs b/codeless/server/src/caps.rs index b2fad6732..3d89c64a9 100644 --- a/codeless/server/src/caps.rs +++ b/codeless/server/src/caps.rs @@ -1,7 +1,20 @@ -use languageserver_types::ServerCapabilities; +use languageserver_types::{ + ServerCapabilities, + TextDocumentSyncCapability, + TextDocumentSyncOptions, + TextDocumentSyncKind, +}; pub const SERVER_CAPABILITIES: ServerCapabilities = ServerCapabilities { - text_document_sync: None, + text_document_sync: Some(TextDocumentSyncCapability::Options( + TextDocumentSyncOptions { + open_close: Some(true), + change: Some(TextDocumentSyncKind::Full), + will_save: None, + will_save_wait_until: None, + save: None, + } + )), hover_provider: None, completion_provider: None, signature_help_provider: None, diff --git a/codeless/server/src/dispatch.rs b/codeless/server/src/dispatch.rs index ee87fa6c3..41437b62a 100644 --- a/codeless/server/src/dispatch.rs +++ b/codeless/server/src/dispatch.rs @@ -9,8 +9,8 @@ use drop_bomb::DropBomb; use ::{ Result, - req::Request, - io::{Io, RawMsg, RawResponse, RawRequest}, + req::{Request, Notification}, + io::{Io, RawMsg, RawResponse, RawRequest, RawNotification}, }; pub struct Responder { @@ -52,7 +52,7 @@ impl Responder } -pub fn parse_as(raw: RawRequest) -> Result<::std::result::Result<(R::Params, Responder), RawRequest>> +pub fn parse_request_as(raw: RawRequest) -> Result<::std::result::Result<(R::Params, Responder), RawRequest>> where R: Request, R::Params: DeserializeOwned, @@ -71,13 +71,13 @@ pub fn parse_as(raw: RawRequest) -> Result<::std::result::Result<(R::Params, Ok(Ok((params, responder))) } -pub fn expect(io: &mut Io, raw: RawRequest) -> Result)>> +pub fn expect_request(io: &mut Io, raw: RawRequest) -> Result)>> where R: Request, R::Params: DeserializeOwned, R::Result: Serialize, { - let ret = match parse_as::(raw)? { + let ret = match parse_request_as::(raw)? { Ok(x) => Some(x), Err(raw) => { unknown_method(io, raw)?; @@ -87,6 +87,37 @@ pub fn expect(io: &mut Io, raw: RawRequest) -> Result(raw: RawNotification) -> Result<::std::result::Result> + where + N: Notification, + N::Params: DeserializeOwned, +{ + if raw.method != N::METHOD { + return Ok(Err(raw)); + } + let params: N::Params = serde_json::from_value(raw.params)?; + Ok(Ok(params)) +} + +pub fn handle_notification(not: &mut Option, f: F) -> Result<()> + where + N: Notification, + N::Params: DeserializeOwned, + F: FnOnce(N::Params) -> Result<()> +{ + match not.take() { + None => Ok(()), + Some(n) => match parse_notification_as::(n)? { + Ok(params) => f(params), + Err(n) => { + *not = Some(n); + Ok(()) + }, + } + } +} + + pub fn unknown_method(io: &mut Io, raw: RawRequest) -> Result<()> { error(io, raw.id, ErrorCode::MethodNotFound, "unknown method") } diff --git a/codeless/server/src/handlers.rs b/codeless/server/src/handlers.rs new file mode 100644 index 000000000..3f257941a --- /dev/null +++ b/codeless/server/src/handlers.rs @@ -0,0 +1,13 @@ +use libanalysis::World; +use libeditor; +use {req, Result}; + +pub fn handle_syntax_tree( + world: World, + params: req::SyntaxTreeParams +) -> Result { + let path = params.text_document.uri.to_file_path() + .map_err(|()| format_err!("invalid path"))?; + let file = world.file_syntax(&path)?; + Ok(libeditor::syntax_tree(&file)) +} diff --git a/codeless/server/src/main.rs b/codeless/server/src/main.rs index fdb2fe2d5..287d650fa 100644 --- a/codeless/server/src/main.rs +++ b/codeless/server/src/main.rs @@ -19,20 +19,25 @@ mod io; mod caps; mod req; mod dispatch; +mod handlers; + +use std::path::PathBuf; use threadpool::ThreadPool; use crossbeam_channel::{bounded, Sender, Receiver}; use flexi_logger::Logger; use libanalysis::WorldState; +use languageserver_types::{TextDocumentItem, VersionedTextDocumentIdentifier, TextDocumentIdentifier}; use ::{ io::{Io, RawMsg}, + handlers::handle_syntax_tree, }; pub type Result = ::std::result::Result; fn main() -> Result<()> { - Logger::with_env_or_str("m=trace") + Logger::with_env_or_str("m=trace, libanalysis=trace") .log_to_file() .directory("log") .start()?; @@ -70,7 +75,7 @@ fn initialize(io: &mut Io) -> Result<()> { loop { match io.recv()? { RawMsg::Request(req) => { - if let Some((_params, resp)) = dispatch::expect::(io, req)? { + if let Some((_params, resp)) = dispatch::expect_request::(io, req)? { resp.result(io, req::InitializeResult { capabilities: caps::SERVER_CAPABILITIES })?; @@ -148,18 +153,12 @@ fn main_loop( match msg { RawMsg::Request(req) => { - let req = match dispatch::parse_as::(req)? { + let req = match dispatch::parse_request_as::(req)? { Ok((params, resp)) => { let world = world.snapshot(); let sender = sender.clone(); pool.execute(move || { - let res: Result = (|| { - let path = params.text_document.uri.to_file_path() - .map_err(|()| format_err!("invalid path"))?; - let file = world.file_syntax(&path)?; - Ok(libeditor::syntax_tree(&file)) - })(); - + let res: Result = handle_syntax_tree(world, params); sender.send(Box::new(|io: &mut Io| resp.response(io, res))) }); continue; @@ -167,12 +166,38 @@ fn main_loop( Err(req) => req, }; - if let Some(((), resp)) = dispatch::expect::(io, req)? { - info!("shutdown request"); + if let Some(((), resp)) = dispatch::expect_request::(io, req)? { + info!("clean shutdown started"); resp.result(io, ())?; return Ok(()); } } + RawMsg::Notification(not) => { + use dispatch::handle_notification as h; + let mut not = Some(not); + h::(&mut not, |params| { + let path = params.text_document.file_path()?; + world.change_overlay(path, Some(params.text_document.text)); + Ok(()) + })?; + h::(&mut not, |mut params| { + let path = params.text_document.file_path()?; + let text = params.content_changes.pop() + .ok_or_else(|| format_err!("empty changes"))? + .text; + world.change_overlay(path, Some(text)); + Ok(()) + })?; + h::(&mut not, |params| { + let path = params.text_document.file_path()?; + world.change_overlay(path, None); + Ok(()) + })?; + + if let Some(not) = not { + error!("unhandled notification: {:?}", not) + } + } msg => { eprintln!("msg = {:?}", msg); } @@ -180,7 +205,6 @@ fn main_loop( } } - trait FnBox: Send { fn call_box(self: Box, a: A) -> R; } @@ -190,3 +214,28 @@ impl R + Send> FnBox for F { (*self)(a) } } + +trait FilePath { + fn file_path(&self) -> Result; +} + +impl FilePath for TextDocumentItem { + fn file_path(&self) -> Result { + self.uri.to_file_path() + .map_err(|()| format_err!("invalid uri: {}", self.uri)) + } +} + +impl FilePath for VersionedTextDocumentIdentifier { + fn file_path(&self) -> Result { + self.uri.to_file_path() + .map_err(|()| format_err!("invalid uri: {}", self.uri)) + } +} + +impl FilePath for TextDocumentIdentifier { + fn file_path(&self) -> Result { + self.uri.to_file_path() + .map_err(|()| format_err!("invalid uri: {}", self.uri)) + } +} diff --git a/codeless/server/src/req.rs b/codeless/server/src/req.rs index 35a20a229..ee4a786c7 100644 --- a/codeless/server/src/req.rs +++ b/codeless/server/src/req.rs @@ -1,6 +1,9 @@ -use languageserver_types::TextDocumentIdentifier; -pub use languageserver_types::request::*; -pub use languageserver_types::{InitializeResult}; +use languageserver_types::{TextDocumentIdentifier, Range}; + +pub use languageserver_types::{ + request::*, notification::*, + InitializeResult, +}; pub enum SyntaxTree {} @@ -11,7 +14,21 @@ impl Request for SyntaxTree { } #[derive(Deserialize, Debug)] -#[serde(rename_all="camelCase")] +#[serde(rename_all = "camelCase")] pub struct SyntaxTreeParams { pub text_document: TextDocumentIdentifier } + +pub enum ExtendSelection {} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ExtendSelectionParams { + pub text_document: TextDocumentIdentifier, + pub selections: Vec, +} + + +pub struct ExtendSelectionResult { + pub selections: Vec, +} diff --git a/codeless/src/extension.ts b/codeless/src/extension.ts index c64065e6b..792af5a73 100644 --- a/codeless/src/extension.ts +++ b/codeless/src/extension.ts @@ -6,7 +6,8 @@ import { ServerOptions, TransportKind, Executable, - TextDocumentIdentifier + TextDocumentIdentifier, + Range } from 'vscode-languageclient'; @@ -18,6 +19,7 @@ let uris = { export function activate(context: vscode.ExtensionContext) { + let textDocumentContentProvider = new TextDocumentContentProvider() let dispose = (disposable) => { context.subscriptions.push(disposable); } @@ -26,11 +28,39 @@ export function activate(context: vscode.ExtensionContext) { } registerCommand('libsyntax-rust.syntaxTree', () => openDoc(uris.syntaxTree)) + registerCommand('libsyntax-rust.extendSelection', async () => { + let editor = vscode.window.activeTextEditor + if (editor == null || editor.document.languageId != "rust") return + let request: ExtendSelectionParams = { + textDocument: { uri: editor.document.uri.toString() }, + selections: editor.selections.map((s) => { + let r: Range = { start: s.start, end: s.end } + return r; + }) + } + let response = await client.sendRequest("m/extendSelection", request) + editor.selections = response.selections.map((range) => { + return new vscode.Selection( + new vscode.Position(range.start.line, range.start.character), + new vscode.Position(range.end.line, range.end.character), + ) + }) + }) + dispose(vscode.workspace.registerTextDocumentContentProvider( 'libsyntax-rust', - new TextDocumentContentProvider() + textDocumentContentProvider )) startServer() + vscode.workspace.onDidChangeTextDocument((event: vscode.TextDocumentChangeEvent) => { + let doc = event.document + if (doc.languageId != "rust") return + // We need to order this after LS updates, but there's no API for that. + // Hence, good old setTimeout. + setTimeout(() => { + textDocumentContentProvider.eventEmitter.fire(uris.syntaxTree) + }, 10) + }, null, context.subscriptions) } export function deactivate(): Thenable { @@ -76,11 +106,28 @@ class TextDocumentContentProvider implements vscode.TextDocumentContentProvider public provideTextDocumentContent(uri: vscode.Uri): vscode.ProviderResult { let editor = vscode.window.activeTextEditor; if (editor == null) return "" - let textDocument: TextDocumentIdentifier = { uri: editor.document.uri.toString() }; - return client.sendRequest("m/syntaxTree", { textDocument }) + let request: SyntaxTreeParams = { + textDocument: { uri: editor.document.uri.toString() } + }; + return client.sendRequest("m/syntaxTree", request); } get onDidChange(): vscode.Event { return this.eventEmitter.event } } + +interface SyntaxTreeParams { + textDocument: TextDocumentIdentifier; +} + +type SyntaxTreeResult = string + +interface ExtendSelectionParams { + textDocument: TextDocumentIdentifier; + selections: Range[]; +} + +interface ExtendSelectionResult { + selections: Range[]; +} diff --git a/libanalysis/Cargo.toml b/libanalysis/Cargo.toml index bde5043e8..737463258 100644 --- a/libanalysis/Cargo.toml +++ b/libanalysis/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" authors = ["Aleksey Kladov "] [dependencies] +log = "0.4.2" failure = "0.1.2" parking_lot = "0.6.3" libsyntax2 = { path = "../" } diff --git a/libanalysis/src/lib.rs b/libanalysis/src/lib.rs index 07e7d567d..417a544ca 100644 --- a/libanalysis/src/lib.rs +++ b/libanalysis/src/lib.rs @@ -1,6 +1,10 @@ extern crate failure; -extern crate libsyntax2; extern crate parking_lot; +#[macro_use] +extern crate log; +extern crate libsyntax2; + +mod arena; use std::{ fs, @@ -66,8 +70,10 @@ impl World { return Ok(file.clone()); } } - - let file = self.with_file_text(path, ast::File::parse)?; + let file = self.with_file_text(path, |text| { + trace!("parsing file: {}", path.display()); + ast::File::parse(text) + })?; let mut guard = self.data.file_map.write(); let file = guard.entry(path.to_owned()) .or_insert(file) @@ -86,7 +92,7 @@ impl World { return Ok(f(&*text)); } } - + trace!("loading file from disk: {}", path.display()); let text = fs::read_to_string(path)?; { let mut guard = self.data.fs_map.write(); diff --git a/libeditor/Cargo.toml b/libeditor/Cargo.toml index 750d80038..fedcf790a 100644 --- a/libeditor/Cargo.toml +++ b/libeditor/Cargo.toml @@ -5,5 +5,6 @@ authors = ["Aleksey Kladov "] publish = false [dependencies] -libsyntax2 = { path = "../" } itertools = "0.7.8" +superslice = "0.1.0" +libsyntax2 = { path = "../" } diff --git a/libeditor/src/lib.rs b/libeditor/src/lib.rs index 4e9631a8b..9da71743f 100644 --- a/libeditor/src/lib.rs +++ b/libeditor/src/lib.rs @@ -1,6 +1,8 @@ extern crate libsyntax2; +extern crate superslice; mod extend_selection; +mod line_index; use libsyntax2::{ SyntaxNodeRef, AstNode, diff --git a/libeditor/src/line_index.rs b/libeditor/src/line_index.rs new file mode 100644 index 000000000..feb482b32 --- /dev/null +++ b/libeditor/src/line_index.rs @@ -0,0 +1,56 @@ +use superslice::Ext; +use ::{TextUnit}; + +pub struct LineIndex { + newlines: Vec, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct LineCol { + pub line: u32, + pub col: TextUnit, +} + +impl LineIndex { + pub fn new(text: &str) -> LineIndex { + let mut newlines = vec![0.into()]; + let mut curr = 0.into(); + for c in text.chars() { + curr += TextUnit::of_char(c); + if c == '\n' { + newlines.push(curr); + } + } + LineIndex { newlines } + } + + pub fn translate(&self, offset: TextUnit) -> LineCol { + let line = self.newlines.upper_bound(&offset) - 1; + let line_start_offset = self.newlines[line]; + let col = offset - line_start_offset; + return LineCol { line: line as u32, col } + } +} + +#[test] +fn test_line_index() { + let text = "hello\nworld"; + let index = LineIndex::new(text); + assert_eq!(index.translate(0.into()), LineCol { line: 0, col: 0.into()}); + assert_eq!(index.translate(1.into()), LineCol { line: 0, col: 1.into()}); + assert_eq!(index.translate(5.into()), LineCol { line: 0, col: 5.into()}); + assert_eq!(index.translate(6.into()), LineCol { line: 1, col: 0.into()}); + assert_eq!(index.translate(7.into()), LineCol { line: 1, col: 1.into()}); + assert_eq!(index.translate(8.into()), LineCol { line: 1, col: 2.into()}); + assert_eq!(index.translate(10.into()), LineCol { line: 1, col: 4.into()}); + assert_eq!(index.translate(11.into()), LineCol { line: 1, col: 5.into()}); + assert_eq!(index.translate(12.into()), LineCol { line: 1, col: 6.into()}); + + let text = "\nhello\nworld"; + let index = LineIndex::new(text); + assert_eq!(index.translate(0.into()), LineCol { line: 0, col: 0.into()}); + assert_eq!(index.translate(1.into()), LineCol { line: 1, col: 0.into()}); + assert_eq!(index.translate(2.into()), LineCol { line: 1, col: 1.into()}); + assert_eq!(index.translate(6.into()), LineCol { line: 1, col: 5.into()}); + assert_eq!(index.translate(7.into()), LineCol { line: 2, col: 0.into()}); +} -- cgit v1.2.3