Skip to content

wasm demo completions support #2

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
Sep 21, 2019
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
13 changes: 13 additions & 0 deletions wasm-demo/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions wasm-demo/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ console_log = "0.1"
log = "0.4"
wasm-bindgen = "0.2.50"
serde = { version = "1.0", features = ["derive"] }
serde_repr = "0.1"
serde-wasm-bindgen = "0.1"

ra_ide_api = { git = "https://github.com/rust-analyzer/rust-analyzer", features = ["wasm"] }
ra_syntax = { git = "https://github.com/rust-analyzer/rust-analyzer" }
ra_text_edit = { git = "https://github.com/rust-analyzer/rust-analyzer" }
3 changes: 3 additions & 0 deletions wasm-demo/rustfmt.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
reorder_modules = false
use_small_heuristics = "Max"
newline_style = "Unix"
134 changes: 134 additions & 0 deletions wasm-demo/src/conv.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
use super::return_types;
use ra_ide_api::{
CompletionItem, CompletionItemKind, FileId, FilePosition, InsertTextFormat, LineCol, LineIndex,
};
use ra_syntax::TextRange;
use ra_text_edit::AtomTextEdit;

pub trait Conv {
type Output;
fn conv(self) -> Self::Output;
}

pub trait ConvWith<CTX> {
type Output;
fn conv_with(self, ctx: CTX) -> Self::Output;
}

#[derive(Clone, Copy)]
pub struct Position {
pub line_number: u32,
pub column: u32,
}

impl ConvWith<(&LineIndex, FileId)> for Position {
type Output = FilePosition;

fn conv_with(self, (line_index, file_id): (&LineIndex, FileId)) -> Self::Output {
let line_col = LineCol { line: self.line_number - 1, col_utf16: self.column - 1 };
let offset = line_index.offset(line_col);
FilePosition { file_id, offset }
}
}

impl ConvWith<&LineIndex> for TextRange {
type Output = return_types::Range;

fn conv_with(self, line_index: &LineIndex) -> Self::Output {
let start = line_index.line_col(self.start());
let end = line_index.line_col(self.end());

return_types::Range {
startLineNumber: start.line + 1,
startColumn: start.col_utf16 + 1,
endLineNumber: end.line + 1,
endColumn: end.col_utf16 + 1,
}
}
}

impl Conv for CompletionItemKind {
type Output = return_types::CompletionItemKind;

fn conv(self) -> <Self as Conv>::Output {
use return_types::CompletionItemKind::*;
match self {
CompletionItemKind::Keyword => Keyword,
CompletionItemKind::Snippet => Snippet,
CompletionItemKind::Module => Module,
CompletionItemKind::Function => Function,
CompletionItemKind::Struct => Struct,
CompletionItemKind::Enum => Enum,
CompletionItemKind::EnumVariant => EnumMember,
CompletionItemKind::BuiltinType => Struct,
CompletionItemKind::Binding => Variable,
CompletionItemKind::Field => Field,
CompletionItemKind::Trait => Interface,
CompletionItemKind::TypeAlias => Struct,
CompletionItemKind::Const => Constant,
CompletionItemKind::Static => Value,
CompletionItemKind::Method => Method,
CompletionItemKind::TypeParam => TypeParameter,
CompletionItemKind::Macro => Method,
}
}
}

impl ConvWith<&LineIndex> for &AtomTextEdit {
type Output = return_types::TextEdit;

fn conv_with(self, line_index: &LineIndex) -> Self::Output {
let text = self.insert.clone();
return_types::TextEdit { range: self.delete.conv_with(line_index), text }
}
}

impl ConvWith<&LineIndex> for CompletionItem {
type Output = return_types::CompletionItem;

fn conv_with(self, line_index: &LineIndex) -> Self::Output {
let mut additional_text_edits = Vec::new();
let mut text_edit = None;
// LSP does not allow arbitrary edits in completion, so we have to do a
// non-trivial mapping here.
for atom_edit in self.text_edit().as_atoms() {
if self.source_range().is_subrange(&atom_edit.delete) {
text_edit = Some(if atom_edit.delete == self.source_range() {
atom_edit.conv_with(line_index)
} else {
assert!(self.source_range().end() == atom_edit.delete.end());
let range1 =
TextRange::from_to(atom_edit.delete.start(), self.source_range().start());
let range2 = self.source_range();
let edit1 = AtomTextEdit::replace(range1, String::new());
let edit2 = AtomTextEdit::replace(range2, atom_edit.insert.clone());
additional_text_edits.push(edit1.conv_with(line_index));
edit2.conv_with(line_index)
})
} else {
assert!(self.source_range().intersection(&atom_edit.delete).is_none());
additional_text_edits.push(atom_edit.conv_with(line_index));
}
}
let return_types::TextEdit { range, text } = text_edit.unwrap();

return_types::CompletionItem {
kind: self.kind().unwrap_or(CompletionItemKind::Struct).conv(),
label: self.label().to_string(),
range,
detail: self.detail().map(|it| it.to_string()),
insertText: text,
insertTextRules: match self.insert_text_format() {
InsertTextFormat::PlainText => return_types::CompletionItemInsertTextRule::None,
InsertTextFormat::Snippet => {
return_types::CompletionItemInsertTextRule::InsertAsSnippet
}
},
documentation: self
.documentation()
.map(|doc| return_types::MarkdownString { value: doc.as_str().to_string() }),
filterText: self.lookup().to_string(),
additionalTextEdits: additional_text_edits,
}
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels bad that we need to literally duplicate this between ra_lsp_server and wasm demo, but still, I think its the right thing to do. Wrapping rust-analyzer into some front-end is a ton of boilerplate (b/c the API surface is large), but it is still much easier than implementing the "compiler" itself.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There might be some opportunity for reducing the code duplication, but that would probably require having an entire "lsp-types"-like crate for monaco.
This way rust-analyzer could have an intermediate crate mapping ra-ide to lsp, which both lsp-server and the wasm demo could use.
It sounds like too much effort to me, including the fact that monaco itself isn't stable and changes API regularly and does everything just a little differently.

85 changes: 46 additions & 39 deletions wasm-demo/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#![cfg(target_arch = "wasm32")]
#![allow(non_snake_case)]

use ra_ide_api::{Analysis, FileId, FilePosition, LineCol, Severity};
use ra_syntax::{SyntaxKind, TextRange};
use ra_ide_api::{Analysis, FileId, FilePosition, Severity};
use ra_syntax::SyntaxKind;
use wasm_bindgen::prelude::*;

mod conv;
use conv::*;
mod return_types;
use return_types::*;

Expand All @@ -30,16 +32,19 @@ impl WorldState {
}

pub fn update(&mut self, code: String) -> JsValue {
log::warn!("update");
let (analysis, file_id) = Analysis::from_single_file(code);
self.analysis = analysis;
self.file_id = file_id;

let line_index = self.analysis.file_line_index(self.file_id).unwrap();

let highlights: Vec<_> = self
.analysis
.highlight(file_id)
.unwrap()
.into_iter()
.map(|hl| Highlight { tag: Some(hl.tag), range: self.range(hl.range) })
.map(|hl| Highlight { tag: Some(hl.tag), range: hl.range.conv_with(&line_index) })
.collect();

let diagnostics: Vec<_> = self
Expand All @@ -49,7 +54,7 @@ impl WorldState {
.into_iter()
.map(|d| {
let Range { startLineNumber, startColumn, endLineNumber, endColumn } =
self.range(d.range);
d.range.conv_with(&line_index);
Diagnostic {
message: d.message,
severity: match d.severity {
Expand All @@ -67,51 +72,42 @@ impl WorldState {
serde_wasm_bindgen::to_value(&UpdateResult { diagnostics, highlights }).unwrap()
}

fn file_pos(&self, line: u32, col_utf16: u32) -> FilePosition {
// monaco doesn't work zero-based
let line_col = LineCol { line: line - 1, col_utf16: col_utf16 - 1 };
let offset = self.analysis.file_line_index(self.file_id).unwrap().offset(line_col);
FilePosition { file_id: self.file_id, offset }
}

fn range(&self, text_range: TextRange) -> Range {
pub fn completions(&self, line_number: u32, column: u32) -> JsValue {
log::warn!("completions");
let line_index = self.analysis.file_line_index(self.file_id).unwrap();
let start = line_index.line_col(text_range.start());
let end = line_index.line_col(text_range.end());

Range {
startLineNumber: start.line + 1,
startColumn: start.col_utf16 + 1,
endLineNumber: end.line + 1,
endColumn: end.col_utf16 + 1,
}
}

pub fn on_dot_typed(&self, line_number: u32, column: u32) {
let pos = self.file_pos(line_number, column);
log::warn!("on_dot_typed");
let res = self.analysis.on_dot_typed(pos).unwrap();
let pos = Position { line_number, column }.conv_with((&line_index, self.file_id));
let res = match self.analysis.completions(pos).unwrap() {
Some(items) => items,
None => return JsValue::NULL,
};

log::debug!("{:?}", res);
let items: Vec<_> = res.into_iter().map(|item| item.conv_with(&line_index)).collect();
serde_wasm_bindgen::to_value(&items).unwrap()
}

pub fn hover(&self, line_number: u32, column: u32) -> JsValue {
let pos = self.file_pos(line_number, column);
log::warn!("hover");
let line_index = self.analysis.file_line_index(self.file_id).unwrap();

let pos = Position { line_number, column }.conv_with((&line_index, self.file_id));
let info = match self.analysis.hover(pos).unwrap() {
Some(info) => info,
_ => return JsValue::NULL,
};

let value = info.info.to_markup();
let hover =
Hover { contents: vec![MarkdownString { value }], range: self.range(info.range) };
let hover = Hover {
contents: vec![MarkdownString { value }],
range: info.range.conv_with(&line_index),
};

serde_wasm_bindgen::to_value(&hover).unwrap()
}

pub fn code_lenses(&self) -> JsValue {
log::warn!("code_lenses");
let line_index = self.analysis.file_line_index(self.file_id).unwrap();

let results: Vec<_> = self
.analysis
Expand All @@ -137,11 +133,11 @@ impl WorldState {
.info
.iter()
.map(|target| target.focus_range().unwrap_or(target.full_range()))
.map(|range| self.range(range))
.map(|range| range.conv_with(&line_index))
.collect();

Some(CodeLensSymbol {
range: self.range(it.node_range),
range: it.node_range.conv_with(&line_index),
command: Some(Command {
id: "editor.action.showReferences".into(),
title,
Expand All @@ -155,36 +151,44 @@ impl WorldState {
}

pub fn references(&self, line_number: u32, column: u32) -> JsValue {
let pos = self.file_pos(line_number, column);
log::warn!("references");
let line_index = self.analysis.file_line_index(self.file_id).unwrap();

let pos = Position { line_number, column }.conv_with((&line_index, self.file_id));
let info = match self.analysis.find_all_refs(pos).unwrap() {
Some(info) => info,
_ => return JsValue::NULL,
};

let res: Vec<_> =
info.into_iter().map(|r| Highlight { tag: None, range: self.range(r.range) }).collect();
let res: Vec<_> = info
.into_iter()
.map(|r| Highlight { tag: None, range: r.range.conv_with(&line_index) })
.collect();
serde_wasm_bindgen::to_value(&res).unwrap()
}

pub fn prepare_rename(&self, line_number: u32, column: u32) -> JsValue {
let pos = self.file_pos(line_number, column);
log::warn!("prepare_rename");
let line_index = self.analysis.file_line_index(self.file_id).unwrap();

let pos = Position { line_number, column }.conv_with((&line_index, self.file_id));
let refs = match self.analysis.find_all_refs(pos).unwrap() {
None => return JsValue::NULL,
Some(refs) => refs,
};

let declaration = refs.declaration();
let range = self.range(declaration.range());
let range = declaration.range().conv_with(&line_index);
let text = declaration.name().to_string();

serde_wasm_bindgen::to_value(&RenameLocation { range, text }).unwrap()
}

pub fn rename(&self, line_number: u32, column: u32, new_name: &str) -> JsValue {
let pos = self.file_pos(line_number, column);
log::warn!("rename");
let line_index = self.analysis.file_line_index(self.file_id).unwrap();

let pos = Position { line_number, column }.conv_with((&line_index, self.file_id));
let change = match self.analysis.rename(pos, new_name) {
Ok(Some(change)) => change,
_ => return JsValue::NULL,
Expand All @@ -195,7 +199,10 @@ impl WorldState {
.source_file_edits
.iter()
.flat_map(|sfe| sfe.edit.as_atoms())
.map(|atom| TextEdit { range: self.range(atom.delete), text: atom.insert.clone() })
.map(|atom| TextEdit {
range: atom.delete.conv_with(&line_index),
text: atom.insert.clone(),
})
.collect();

serde_wasm_bindgen::to_value(&result).unwrap()
Expand Down
Loading