use std::panic::AssertUnwindSafe;
use std::path::Path;
use std::sync::Arc;
use lsp_types::{
CompletionItem, CompletionItemKind, CompletionTextEdit, Documentation, InsertReplaceEdit,
MarkupContent, MarkupKind, Position, TextEdit,
};
use rowan::TextSize;
use serde::{Deserialize, Serialize};
use crate::incremental::Analysis;
use crate::index::{ModuleIndex, PackageIndex};
use crate::parser::{KEYWORDS, parse};
use crate::resolve::{
Candidate, ModulePath, Namespace, PackageSource, Resolver, Source, resolve_submodule,
};
use crate::semantic::{BindingKind, SemanticModel};
use crate::syntax::{SyntaxKind, SyntaxNode};
use crate::text::{LineIndex, PositionEncoding, TextBuffer};
use super::latex_symbols::{EMOJI_SYMBOLS, LATEX_SYMBOLS};
use super::render::{binding_detail, function_detail, type_detail};
use super::symbols::token_at;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ResolveData {
module_path: Vec<String>,
name: String,
}
pub fn compute_completions<P: PackageSource>(
text: &str,
position: Position,
encoding: PositionEncoding,
packages: &P,
) -> Vec<CompletionItem> {
let offset = LineIndex::new(text).position_to_byte(position, encoding);
let root = parse(text).cst;
let model = SemanticModel::build(&root);
completions_for(
&model,
&root,
packages,
None,
text,
TextSize::new(offset as u32),
encoding,
)
}
pub(crate) fn completion_via_db(
snapshot: &Analysis,
path: &Path,
text: &TextBuffer,
position: Position,
encoding: PositionEncoding,
) -> Vec<CompletionItem> {
let offset = TextSize::new(text.line_index().position_to_byte(position, encoding) as u32);
let cached = salsa::Cancelled::catch(AssertUnwindSafe(|| {
let file = snapshot.lookup_file(path)?;
if snapshot.file_text(file) != text {
return None;
}
let root = snapshot.parsed_tree(file);
let model = snapshot.semantic_model(file);
let workspace = snapshot.workspace_member(path);
Some(completions_for(
model, &root, snapshot, workspace, text, offset, encoding,
))
}));
match cached {
Ok(Some(items)) => items,
Ok(None) | Err(_) => compute_completions(text, position, encoding, snapshot),
}
}
pub(crate) fn resolve_completion(snapshot: &Analysis, item: CompletionItem) -> CompletionItem {
resolve_completion_with(
&|name| snapshot.library_package(name).map(|p| (*p).clone()),
item,
)
}
fn completions_for<P: PackageSource>(
model: &SemanticModel,
root: &SyntaxNode,
packages: &P,
workspace: Option<(Arc<PackageIndex>, ModulePath)>,
text: &str,
offset: TextSize,
encoding: PositionEncoding,
) -> Vec<CompletionItem> {
let offset_bytes: usize = offset.into();
if let Some(context) = super::documentation::DocumentationContext::at(model, offset) {
if let Some(embedded) = context.embedded_julia() {
let parsed = parse(embedded.text);
let embedded_model = SemanticModel::build(&parsed.cst);
let mut items = completions_for(
&embedded_model,
&parsed.cst,
packages,
workspace.clone(),
embedded.text,
embedded.offset,
encoding,
);
let outer = reference_completions(
model,
packages,
workspace,
embedded.text,
embedded.offset,
context.attachment.target_range.start(),
false,
);
let mut seen: std::collections::HashSet<String> =
items.iter().map(|item| item.label.clone()).collect();
items.extend(
outer
.into_iter()
.filter(|item| seen.insert(item.label.clone())),
);
return map_embedded_edits(items, &embedded, text, encoding);
}
if let Some(reference) = context.explicit_ref() {
let replacement = context.source_range(reference.range).map(|range| {
super::documentation::lsp_range(range, &LineIndex::new(text), encoding)
});
let mut items = reference_completions(
model,
packages,
workspace,
reference.target.as_str(),
reference.offset,
context.attachment.target_range.start(),
true,
);
if let Some(range) = replacement {
for item in &mut items {
if item.kind == Some(CompletionItemKind::REFERENCE) {
item.text_edit = Some(CompletionTextEdit::Edit(TextEdit {
range,
new_text: item.label.clone(),
}));
}
}
}
return items;
}
if let Context::Latex { start } = context_at(text, offset_bytes) {
let typed = &text[start..offset_bytes];
if !is_lone_escape(&typed[1..]) {
return latex_items(text, start, offset_bytes, encoding);
}
}
return Vec::new();
}
let context = context_at(text, offset_bytes);
match context {
Context::Latex { start } => {
let typed = &text[start..offset_bytes];
let suppressed = match string_context_at(root, offset) {
StringContext::Verbatim => true,
StringContext::Plain => is_lone_escape(&typed[1..]),
StringContext::Code => false,
};
if suppressed {
Vec::new()
} else {
latex_items(text, start, offset_bytes, encoding)
}
}
_ if string_context_at(root, offset) != StringContext::Code
&& in_documentation_payload(model, offset) =>
{
Vec::new()
}
Context::Member {
receiver,
macro_member,
} => member_completions(packages, workspace.as_ref(), &receiver, macro_member),
Context::Macro => Resolver::new(model, packages)
.with_workspace(workspace)
.visible(offset, Namespace::Macro)
.into_iter()
.map(|c| candidate_item(model, c, Namespace::Macro))
.collect(),
Context::Value => {
let mut items: Vec<CompletionItem> = Resolver::new(model, packages)
.with_workspace(workspace)
.visible(offset, Namespace::Value)
.into_iter()
.map(|c| candidate_item(model, c, Namespace::Value))
.collect();
items.extend(KEYWORDS.iter().map(|kw| keyword_item(kw)));
items
}
}
}
#[allow(clippy::too_many_arguments)]
fn reference_completions<P: PackageSource>(
model: &SemanticModel,
packages: &P,
workspace: Option<(Arc<PackageIndex>, ModulePath)>,
target: &str,
offset: TextSize,
semantic_offset: TextSize,
anchors: bool,
) -> Vec<CompletionItem> {
match context_at(target, offset.into()) {
Context::Member {
receiver,
macro_member,
} => member_completions(packages, workspace.as_ref(), &receiver, macro_member),
Context::Macro => Resolver::new(model, packages)
.with_workspace(workspace)
.visible(semantic_offset, Namespace::Macro)
.into_iter()
.map(|candidate| candidate_item(model, candidate, Namespace::Macro))
.collect(),
Context::Value => {
let mut items: Vec<_> = Resolver::new(model, packages)
.with_workspace(workspace)
.visible(semantic_offset, Namespace::Value)
.into_iter()
.map(|candidate| candidate_item(model, candidate, Namespace::Value))
.collect();
let anchor_names = if anchors {
super::documentation::markdown_anchor_names(model)
} else {
Vec::new()
};
for anchor in anchor_names {
if items.iter().any(|item| item.label == anchor) {
continue;
}
items.push(CompletionItem {
label: anchor,
kind: Some(CompletionItemKind::REFERENCE),
detail: Some("documentation anchor".to_string()),
..Default::default()
});
}
items
}
Context::Latex { .. } => Vec::new(),
}
}
fn map_embedded_edits(
mut items: Vec<CompletionItem>,
embedded: &super::documentation::EmbeddedJulia<'_>,
source: &str,
encoding: PositionEncoding,
) -> Vec<CompletionItem> {
let embedded_index = LineIndex::new(embedded.text);
let source_index = LineIndex::new(source);
let map = |range| {
embedded
.source_range_from_lsp(range, &embedded_index, encoding)
.map(|range| super::documentation::lsp_range(range, &source_index, encoding))
};
for item in &mut items {
item.text_edit = item.text_edit.take().and_then(|edit| match edit {
CompletionTextEdit::Edit(mut edit) => {
edit.range = map(edit.range)?;
Some(CompletionTextEdit::Edit(edit))
}
CompletionTextEdit::InsertAndReplace(mut edit) => {
edit.insert = map(edit.insert)?;
edit.replace = map(edit.replace)?;
Some(CompletionTextEdit::InsertAndReplace(InsertReplaceEdit {
new_text: edit.new_text,
insert: edit.insert,
replace: edit.replace,
}))
}
});
if let Some(edits) = &mut item.additional_text_edits {
edits.retain_mut(|edit| match map(edit.range) {
Some(range) => {
edit.range = range;
true
}
None => false,
});
}
}
items
}
#[derive(Debug, PartialEq, Eq)]
enum Context {
Value,
Macro,
Member {
receiver: Vec<String>,
macro_member: bool,
},
Latex {
start: usize,
},
}
fn context_at(text: &str, offset: usize) -> Context {
let prefix = &text[..offset.min(text.len())];
if let Some(start) = sequence_start(prefix) {
return Context::Latex { start };
}
let (_word, rest) = take_ident_back(prefix);
let (macro_sigil, rest) = match rest.strip_suffix('@') {
Some(r) => (true, r),
None => (false, rest),
};
if let Some(before_dot) = rest.strip_suffix('.') {
let receiver = scan_dotted(before_dot);
if !receiver.is_empty() {
return Context::Member {
receiver,
macro_member: macro_sigil,
};
}
}
if macro_sigil {
Context::Macro
} else {
Context::Value
}
}
fn take_ident_back(prefix: &str) -> (&str, &str) {
let start = prefix
.char_indices()
.rev()
.take_while(|(_, c)| is_ident_char(*c))
.last()
.map(|(i, _)| i)
.unwrap_or(prefix.len());
(&prefix[start..], &prefix[..start])
}
fn scan_dotted(s: &str) -> Vec<String> {
let mut comps = Vec::new();
let mut cursor = s;
loop {
let (ident, rest) = take_ident_back(cursor);
if ident.is_empty() {
break;
}
comps.push(ident.to_string());
match rest.strip_suffix('.') {
Some(r) => cursor = r,
None => break,
}
}
comps.reverse();
comps
}
fn is_ident_char(c: char) -> bool {
c.is_alphanumeric() || c == '_' || c == '!'
}
const MAX_SEQUENCE_LEN: usize = 43;
fn sequence_start(prefix: &str) -> Option<usize> {
for (i, c) in prefix.char_indices().rev() {
if prefix.len() - i > MAX_SEQUENCE_LEN {
return None;
}
if c == '\\' {
return Some(i);
}
if !is_sequence_char(c) {
return None;
}
}
None
}
fn is_sequence_char(c: char) -> bool {
c.is_ascii_alphanumeric() || "_^/+-()!=<>:".contains(c)
}
fn latex_items(
text: &str,
start: usize,
offset: usize,
encoding: PositionEncoding,
) -> Vec<CompletionItem> {
let typed = &text[start..offset];
let line_index = LineIndex::new(text);
let range = lsp_types::Range {
start: line_index.byte_to_position(start, encoding),
end: line_index.byte_to_position(offset, encoding),
};
let emoji: &[(&str, &str)] = if typed.starts_with("\\:") {
prefixed(EMOJI_SYMBOLS, typed)
} else {
&[]
};
prefixed(LATEX_SYMBOLS, typed)
.iter()
.chain(emoji)
.map(|&(sequence, expansion)| latex_item(sequence, expansion, range))
.collect()
}
fn prefixed<'a>(table: &'a [(&'a str, &'a str)], prefix: &str) -> &'a [(&'a str, &'a str)] {
let start = table.partition_point(|(key, _)| *key < prefix);
let len = table[start..].partition_point(|(key, _)| key.starts_with(prefix));
&table[start..start + len]
}
fn latex_item(sequence: &str, expansion: &str, range: lsp_types::Range) -> CompletionItem {
CompletionItem {
label: sequence.to_string(),
kind: Some(CompletionItemKind::TEXT),
detail: Some(expansion.to_string()),
documentation: Some(Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value: format!("`{expansion}` {}", codepoints(expansion)),
})),
filter_text: Some(sequence.to_string()),
text_edit: Some(CompletionTextEdit::Edit(TextEdit {
range,
new_text: expansion.to_string(),
})),
..Default::default()
}
}
fn codepoints(s: &str) -> String {
s.chars()
.map(|c| format!("U+{:04X}", c as u32))
.collect::<Vec<_>>()
.join(" ")
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum StringContext {
Code,
Plain,
Verbatim,
}
fn in_documentation_payload(model: &SemanticModel, offset: TextSize) -> bool {
model
.documentation()
.iter()
.any(|doc| doc.payload_range.contains_inclusive(offset))
}
fn string_context_at(root: &SyntaxNode, offset: TextSize) -> StringContext {
let Some(token) = token_at(root, offset) else {
return StringContext::Code;
};
token
.parent_ancestors()
.find_map(|node| match node.kind() {
SyntaxKind::INTERPOLATION => Some(StringContext::Code),
SyntaxKind::CMD_LITERAL => Some(StringContext::Verbatim),
SyntaxKind::STRING_LITERAL => Some(
if node
.children_with_tokens()
.any(|c| c.kind() == SyntaxKind::STRING_PREFIX)
{
StringContext::Verbatim
} else {
StringContext::Plain
},
),
_ => None,
})
.unwrap_or(StringContext::Code)
}
fn is_lone_escape(sequence: &str) -> bool {
let mut chars = sequence.chars();
let (Some(c), None) = (chars.next(), chars.next()) else {
return false;
};
c.is_ascii_digit() || "abefnrtv\\'\"?xuU".contains(c)
}
fn candidate_item(model: &SemanticModel, cand: Candidate, ns: Namespace) -> CompletionItem {
let label = cand.name.to_string();
match cand.source {
Source::Binding(id) => {
let kind = model.binding(id).kind;
let mut item = CompletionItem {
label,
kind: Some(binding_kind(kind)),
detail: Some(binding_detail(kind).to_string()),
..Default::default()
};
let doc = model.documentation_for_binding(id).find_map(|doc| {
let crate::ast::DocText::Static(text) = &doc.text else {
return None;
};
Some(text.as_str())
});
set_doc(&mut item, doc);
item
}
Source::Workspace { module } | Source::Using { module } | Source::System { module } => {
library_item(label, &[module.to_string()], ns)
}
}
}
fn library_item(name: String, module_path: &[String], ns: Namespace) -> CompletionItem {
let detail = module_path.last().cloned();
CompletionItem {
label: name.clone(),
kind: Some(heuristic_kind(&name, ns)),
detail,
data: resolve_data(module_path, &name),
..Default::default()
}
}
fn keyword_item(kw: &str) -> CompletionItem {
CompletionItem {
label: kw.to_string(),
kind: Some(CompletionItemKind::KEYWORD),
..Default::default()
}
}
fn resolve_data(module_path: &[String], name: &str) -> Option<serde_json::Value> {
serde_json::to_value(ResolveData {
module_path: module_path.to_vec(),
name: name.to_string(),
})
.ok()
}
fn binding_kind(kind: BindingKind) -> CompletionItemKind {
use BindingKind::*;
match kind {
Global | Local | ForVar | LetVar => CompletionItemKind::VARIABLE,
Const => CompletionItemKind::CONSTANT,
Param | KeywordParam | CatchParam => CompletionItemKind::VARIABLE,
TypeParam => CompletionItemKind::TYPE_PARAMETER,
Field => CompletionItemKind::FIELD,
Function => CompletionItemKind::FUNCTION,
Macro => CompletionItemKind::FUNCTION,
Type => CompletionItemKind::CLASS,
Module => CompletionItemKind::MODULE,
Import => CompletionItemKind::MODULE,
}
}
fn heuristic_kind(name: &str, ns: Namespace) -> CompletionItemKind {
if ns == Namespace::Macro {
return CompletionItemKind::FUNCTION;
}
match name.chars().next() {
Some(c) if c.is_uppercase() => CompletionItemKind::CLASS,
_ => CompletionItemKind::FUNCTION,
}
}
fn member_completions<P: PackageSource>(
packages: &P,
workspace: Option<&(Arc<PackageIndex>, ModulePath)>,
receiver: &[String],
macro_member: bool,
) -> Vec<CompletionItem> {
let Some((head, tail)) = receiver.split_first() else {
return Vec::new();
};
let pkg = workspace
.map(|(package, _)| package)
.filter(|package| package.name == *head)
.cloned()
.or_else(|| packages.package(head));
let Some(pkg) = pkg else { return Vec::new() };
let tail: Vec<&str> = tail.iter().map(String::as_str).collect();
let Some(module) = resolve_submodule(&pkg.root, &tail) else {
return Vec::new();
};
member_items(module, receiver, macro_member)
}
fn member_items(module: &ModuleIndex, path: &[String], macro_member: bool) -> Vec<CompletionItem> {
let mut items = Vec::new();
if macro_member {
for m in &module.macros {
items.push(member_item(&m.name, CompletionItemKind::FUNCTION, path));
}
return items;
}
for f in &module.functions {
if f.owner.is_none() {
items.push(member_item(&f.name, CompletionItemKind::FUNCTION, path));
}
}
for t in &module.types {
items.push(member_item(&t.name, CompletionItemKind::CLASS, path));
}
for c in &module.consts {
items.push(member_item(&c.name, CompletionItemKind::CONSTANT, path));
}
for s in &module.submodules {
items.push(member_item(&s.name, CompletionItemKind::MODULE, path));
}
items
}
fn member_item(name: &str, kind: CompletionItemKind, module_path: &[String]) -> CompletionItem {
CompletionItem {
label: name.to_string(),
kind: Some(kind),
detail: module_path.last().cloned(),
data: resolve_data(module_path, name),
..Default::default()
}
}
fn resolve_completion_with(
lookup: &dyn Fn(&str) -> Option<crate::index::PackageIndex>,
mut item: CompletionItem,
) -> CompletionItem {
let Some(data) = item.data.take() else {
return item;
};
let Ok(data) = serde_json::from_value::<ResolveData>(data) else {
return item;
};
let Some((head, tail)) = data.module_path.split_first() else {
return item;
};
let Some(pkg) = lookup(head) else {
return item;
};
let tail: Vec<&str> = tail.iter().map(String::as_str).collect();
let Some(module) = resolve_submodule(&pkg.root, &tail) else {
return item;
};
enrich(&mut item, module, &data.name);
item
}
fn enrich(item: &mut CompletionItem, module: &ModuleIndex, name: &str) {
if name.starts_with('@') {
if let Some(m) = module.macros.iter().find(|m| m.name == name) {
set_doc(item, m.doc.as_ref().map(|d| d.text.as_str()));
}
return;
}
if let Some(f) = module.functions.iter().find(|f| f.name == name) {
item.detail = Some(function_detail(f));
set_doc(item, f.doc.as_ref().map(|d| d.text.as_str()));
return;
}
if let Some(t) = module.types.iter().find(|t| t.name == name) {
item.detail = Some(type_detail(t));
set_doc(item, t.doc.as_ref().map(|d| d.text.as_str()));
return;
}
if let Some(c) = module.consts.iter().find(|c| c.name == name) {
if let Some(repr) = &c.value_repr {
item.detail = Some(format!("{name} = {repr}"));
}
set_doc(item, c.doc.as_ref().map(|d| d.text.as_str()));
}
}
fn set_doc(item: &mut CompletionItem, doc: Option<&str>) {
if let Some(text) = doc.and_then(super::documentation::render) {
item.documentation = Some(Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value: text,
}));
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeMap;
use std::sync::Arc;
use crate::index::model::{DefLocation, ExportedName, PackageIndex, Span, Visibility};
use crate::index::{
ConstDef, FunctionGroup, MacroDef, Method, Param, TypeDef, TypeExpr, TypeKind,
};
fn loc() -> DefLocation {
DefLocation {
file: "src/x.jl".into(),
range: Span { start: 0, end: 0 },
}
}
fn module(name: &str, exports: &[&str]) -> ModuleIndex {
ModuleIndex {
name: name.to_string(),
bare: false,
loc: loc(),
doc: None,
exports: exports
.iter()
.map(|n| ExportedName {
name: n.to_string(),
visibility: Visibility::Exported,
loc: loc(),
})
.collect(),
functions: Vec::new(),
types: Vec::new(),
consts: Vec::new(),
macros: Vec::new(),
submodules: Vec::new(),
usings: Vec::new(),
imported_names: Vec::new(),
}
}
fn package(root: ModuleIndex) -> Arc<PackageIndex> {
Arc::new(PackageIndex {
name: root.name.clone(),
root,
members: Vec::new(),
member_modules: Default::default(),
diagnostics: Vec::new(),
})
}
fn library(pkgs: Vec<Arc<PackageIndex>>) -> BTreeMap<String, Arc<PackageIndex>> {
pkgs.into_iter().map(|p| (p.name.clone(), p)).collect()
}
fn func(name: &str) -> FunctionGroup {
FunctionGroup {
name: name.to_string(),
owner: None,
methods: Vec::new(),
doc: None,
}
}
fn completions_at(
src: &str,
needle: &str,
lib: &BTreeMap<String, Arc<PackageIndex>>,
) -> Vec<CompletionItem> {
let offset = src.find(needle).unwrap() + needle.len();
let line_index = LineIndex::new(src);
let position = line_index.byte_to_position(offset, PositionEncoding::Utf16);
compute_completions(src, position, PositionEncoding::Utf16, lib)
}
fn labels(items: &[CompletionItem]) -> Vec<String> {
items.iter().map(|i| i.label.clone()).collect()
}
fn completions_ws(
src: &str,
needle: &str,
lib: &BTreeMap<String, Arc<PackageIndex>>,
workspace: Arc<PackageIndex>,
) -> Vec<CompletionItem> {
let root = parse(src).cst;
let model = SemanticModel::build(&root);
let offset = TextSize::new((src.find(needle).unwrap() + needle.len()) as u32);
completions_for(
&model,
&root,
lib,
Some((workspace, Vec::new())),
src,
offset,
PositionEncoding::Utf16,
)
}
fn completions_bare(src: &str, needle: &str) -> Vec<CompletionItem> {
completions_at(src, needle, &library(vec![]))
}
fn item<'a>(items: &'a [CompletionItem], label: &str) -> &'a CompletionItem {
items
.iter()
.find(|i| i.label == label)
.unwrap_or_else(|| panic!("no {label:?} in {:?}", labels(items)))
}
fn edit(item: &CompletionItem) -> (&str, lsp_types::Range) {
match item.text_edit.as_ref().expect("a text edit") {
CompletionTextEdit::Edit(e) => (e.new_text.as_str(), e.range),
other => panic!("expected a plain edit, got {other:?}"),
}
}
#[test]
fn value_context_offers_workspace_siblings() {
let lib = library(vec![package(module("Base", &["println"]))]);
let ws = package(ModuleIndex {
functions: vec![func("sibling")],
..module("MyPkg", &[])
});
let src = "function f()\n \nend";
let items = completions_ws(src, " ", &lib, ws);
let names = labels(&items);
assert!(names.contains(&"sibling".to_string()), "{names:?}");
assert!(
names.iter().position(|n| n == "sibling") < names.iter().position(|n| n == "println")
);
}
#[test]
fn value_context_lists_locals_before_library_and_includes_keywords() {
let lib = library(vec![package(module("Base", &["println"]))]);
let src = "function f(a)\n b = 1\n \nend";
let items = completions_at(src, "b = 1\n ", &lib);
let names = labels(&items);
for expected in ["a", "b", "f", "println", "function", "end"] {
assert!(names.contains(&expected.to_string()), "missing {expected}");
}
assert!(names.iter().position(|n| n == "b") < names.iter().position(|n| n == "println"));
let kw = items.iter().find(|i| i.label == "function").unwrap();
assert_eq!(kw.kind, Some(CompletionItemKind::KEYWORD));
}
#[test]
fn local_completion_carries_decoded_documentation() {
let lib = library(vec![]);
let src = "\"A decoded\\n**docstring**.\"\ngreet(name) = name\ngr";
let items = completions_at(src, "\ngr", &lib);
let greet = item(&items, "greet");
match &greet.documentation {
Some(Documentation::MarkupContent(markup)) => {
assert_eq!(markup.value, "A decoded\n**docstring**.")
}
other => panic!("expected decoded markdown documentation, got {other:?}"),
}
}
#[test]
fn explicit_ref_offers_symbols_but_ordinary_prose_does_not() {
let lib = library(vec![]);
let source = concat!(
"\"\"\"\n",
"See [`greet`](@ref gre) and ordinary gre prose.\n",
"\"\"\"\n",
"greet(name) = name\n",
);
let ref_items = completions_at(source, "@ref gre", &lib);
assert!(labels(&ref_items).contains(&"greet".to_string()));
let prose_items = completions_at(source, "ordinary gre", &lib);
assert!(
prose_items.is_empty(),
"prose should not offer Julia names: {:?}",
labels(&prose_items)
);
}
#[test]
fn incomplete_explicit_ref_still_offers_completion() {
let lib = library(vec![]);
let source = concat!(
"\"\"\"\n",
"See [`greet`](@ref gre\n",
"\"\"\"\n",
"greet(name) = name\n",
);
let names = labels(&completions_at(source, "@ref gre", &lib));
assert!(names.contains(&"greet".to_string()), "{names:?}");
}
#[test]
fn embedded_julia_completion_sees_fence_locals() {
let lib = library(vec![]);
let source = concat!(
"\"\"\"\n",
"```julia\n",
"example_value = 1\n",
"example_\n",
"```\n",
"\"\"\"\n",
"f() = 1\n",
);
let names = labels(&completions_at(source, "\nexample_", &lib));
assert!(names.contains(&"example_value".to_string()), "{names:?}");
}
#[test]
fn embedded_julia_completion_sees_the_documented_file_scope() {
let lib = library(vec![]);
let source = concat!(
"\"\"\"\n",
"```julia\n",
"gre\n",
"```\n",
"\"\"\"\n",
"greet(name) = name\n",
);
let names = labels(&completions_at(source, "\ngre", &lib));
assert!(names.contains(&"greet".to_string()), "{names:?}");
}
#[test]
fn explicit_ref_completes_qualified_workspace_members() {
let lib = library(vec![]);
let workspace = package(ModuleIndex {
functions: vec![func("greet")],
..module("MyPkg", &[])
});
let source = concat!(
"\"\"\"\n",
"See [`greet`](@ref MyPkg.gre).\n",
"\"\"\"\n",
"f() = 1\n",
);
let names = labels(&completions_ws(source, "MyPkg.gre", &lib, workspace));
assert!(names.contains(&"greet".to_string()), "{names:?}");
}
#[test]
fn explicit_ref_completes_markdown_anchors() {
let lib = library(vec![]);
let source = concat!(
"\"\"\"\n",
"# [Overview](@id custom-overview)\n",
"\n",
"See [above](@ref custom-ov).\n",
"\"\"\"\n",
"f() = 1\n",
);
let items = completions_at(source, "@ref custom-ov", &lib);
let anchor = item(&items, "custom-overview");
assert_eq!(anchor.kind, Some(CompletionItemKind::REFERENCE));
let (new_text, range) = edit(anchor);
assert_eq!(new_text, "custom-overview");
assert_eq!(range.start, Position::new(3, 17));
assert_eq!(range.end, Position::new(3, 26));
}
#[test]
fn markdown_anchors_stay_out_of_embedded_julia_completion() {
let lib = library(vec![]);
let source = concat!(
"\"\"\"\n",
"# [Overview](@id custom_overview)\n",
"\n",
"```julia\n",
"custom_\n",
"```\n",
"\"\"\"\n",
"f() = 1\n",
);
let names = labels(&completions_at(source, "\ncustom_", &lib));
assert!(
!names.contains(&"custom_overview".to_string()),
"an anchor leaked into Julia code completion: {names:?}"
);
}
#[test]
fn plain_strings_and_commands_keep_identifier_completion() {
let lib = library(vec![]);
for (source, needle) in [
("greeting = 1\npath = \"gree\"\n", "\"gree"),
("greeting = 1\ncmd = `run gree`\n", "run gree"),
] {
let names = labels(&completions_at(source, needle, &lib));
assert!(
names.contains(&"greeting".to_string()),
"{source:?}: {names:?}"
);
}
}
#[test]
fn interpolations_keep_julia_completion_inside_strings() {
let lib = library(vec![]);
let source = "greeting = \"hello\"\nmessage = \"$(gree)\"\n";
let names = labels(&completions_at(source, "$(gree", &lib));
assert!(names.contains(&"greeting".to_string()), "{names:?}");
}
#[test]
fn shadowed_name_appears_once() {
let lib = library(vec![package(module("Base", &["map"]))]);
let src = "function f()\n map = 1\n \nend";
let names = labels(&completions_at(src, "map = 1\n ", &lib));
assert_eq!(names.iter().filter(|n| *n == "map").count(), 1);
}
#[test]
fn macro_context_offers_only_at_names() {
let mut base = module("Base", &["@time", "time"]);
base.macros.push(MacroDef {
name: "@time".into(),
params: Vec::new(),
doc: None,
loc: loc(),
});
let lib = library(vec![package(base)]);
let src = "@t";
let names = labels(&completions_at(src, "@t", &lib));
assert!(names.contains(&"@time".to_string()));
assert!(!names.contains(&"time".to_string()));
}
#[test]
fn member_context_lists_defined_names_and_submodules() {
let mut root = module("A", &[]);
root.functions.push(func("foo"));
root.types.push(TypeDef {
name: "Bar".into(),
kind: TypeKind::Struct { mutable: false },
type_params: Vec::new(),
supertype: None,
fields: Vec::new(),
doc: None,
loc: loc(),
});
root.consts.push(ConstDef {
name: "BAUD".into(),
value_repr: Some("9600".into()),
doc: None,
loc: loc(),
});
root.submodules.push(module("Inner", &[]));
let lib = library(vec![package(root)]);
let items = completions_at("A.", "A.", &lib);
let names = labels(&items);
for expected in ["foo", "Bar", "BAUD", "Inner"] {
assert!(names.contains(&expected.to_string()), "missing {expected}");
}
let bar = items.iter().find(|i| i.label == "Bar").unwrap();
assert_eq!(bar.kind, Some(CompletionItemKind::CLASS));
let inner = items.iter().find(|i| i.label == "Inner").unwrap();
assert_eq!(inner.kind, Some(CompletionItemKind::MODULE));
}
#[test]
fn member_context_walks_a_submodule_chain() {
let mut inner = module("B", &[]);
inner.functions.push(func("deep"));
let mut root = module("A", &[]);
root.submodules.push(inner);
let lib = library(vec![package(root)]);
let names = labels(&completions_at("A.B.", "A.B.", &lib));
assert_eq!(names, vec!["deep".to_string()]);
}
#[test]
fn macro_member_context_offers_only_macros() {
let mut root = module("A", &[]);
root.functions.push(func("plain"));
root.macros.push(MacroDef {
name: "@mac".into(),
params: Vec::new(),
doc: None,
loc: loc(),
});
let lib = library(vec![package(root)]);
let names = labels(&completions_at("A.@", "A.@", &lib));
assert_eq!(names, vec!["@mac".to_string()]);
}
#[test]
fn unknown_receiver_yields_no_members() {
let lib = library(vec![package(module("A", &[]))]);
assert!(completions_at("Nope.", "Nope.", &lib).is_empty());
}
#[test]
fn resolve_fills_docs_and_signature() {
let mut root = module("A", &[]);
let mut group = func("foo");
group.doc = Some(crate::index::Docstring {
text: "does a foo".into(),
loc: loc(),
});
group.methods.push(Method {
params: vec![Param {
name: Some("x".into()),
type_annotation: Some(TypeExpr::Name {
path: vec!["Int".into()],
}),
default: None,
is_vararg: false,
}],
keyword_params: Vec::new(),
type_args: Vec::new(),
where_clauses: Vec::new(),
return_type: None,
has_body: true,
doc: None,
loc: loc(),
});
root.functions.push(group);
let pkg = (*package(root)).clone();
let item = CompletionItem {
label: "foo".into(),
data: resolve_data(&["A".into()], "foo"),
..Default::default()
};
let resolved = resolve_completion_with(&|name| (name == "A").then(|| pkg.clone()), item);
assert_eq!(resolved.detail.as_deref(), Some("foo(x::Int)"));
match resolved.documentation {
Some(Documentation::MarkupContent(m)) => assert_eq!(m.value, "does a foo"),
other => panic!("expected markdown docs, got {other:?}"),
}
}
#[test]
fn tables_fit_the_scanner() {
for table in [LATEX_SYMBOLS, EMOJI_SYMBOLS] {
assert!(!table.is_empty());
for window in table.windows(2) {
assert!(window[0].0 < window[1].0, "unsorted at {:?}", window[0].0);
}
for &(key, expansion) in table {
let rest = key
.strip_prefix('\\')
.unwrap_or_else(|| panic!("{key:?} lacks a backslash"));
assert!(!rest.is_empty(), "{key:?} is a bare backslash");
assert!(
rest.chars().all(is_sequence_char),
"{key:?} has a character the scanner stops at"
);
assert!(
key.len() <= MAX_SEQUENCE_LEN,
"{key:?} outruns the scan cap"
);
assert!(!expansion.is_empty(), "{key:?} expands to nothing");
}
}
}
#[test]
fn latex_sequence_offers_expansions_and_replaces_the_backslash() {
let items = completions_bare("x = \\alph", "\\alph");
let alpha = item(&items, "\\alpha");
assert_eq!(alpha.detail.as_deref(), Some("α"));
assert_eq!(alpha.kind, Some(CompletionItemKind::TEXT));
let (new_text, range) = edit(alpha);
assert_eq!(new_text, "α");
assert_eq!(range.start.character, 4);
assert_eq!(range.end.character, 9);
assert!(!labels(&items).contains(&"\\beta".to_string()));
}
#[test]
fn sequence_directly_after_an_identifier_is_offered() {
let items = completions_bare("x\\_1", "\\_1");
let (new_text, range) = edit(item(&items, "\\_1"));
assert_eq!(new_text, "₁");
assert_eq!(range.start.character, 1);
}
#[test]
fn emoji_sequences_are_offered() {
let items = completions_bare("\\:smi", "\\:smi");
assert_eq!(edit(item(&items, "\\:smile:")).0, "😄");
}
#[test]
fn a_bare_backslash_offers_latex_only() {
let names = labels(&completions_bare("\\", "\\"));
assert!(names.contains(&"\\alpha".to_string()));
assert!(!names.contains(&"\\:smile:".to_string()));
assert_eq!(names.len(), LATEX_SYMBOLS.len());
}
#[test]
fn a_colon_opens_the_emoji_table() {
let names = labels(&completions_bare("\\:", "\\:"));
assert_eq!(names.len(), EMOJI_SYMBOLS.len());
assert!(names.contains(&"\\:smile:".to_string()));
}
#[test]
fn verbatim_strings_offer_nothing() {
for src in [
"m = r\"\\d",
"p = raw\"\\n",
"c = `ls \\d",
"m = match(r\"\\s", ] {
let needle = &src[src.len() - 2..];
assert!(
completions_bare(src, needle).is_empty(),
"expected nothing in {src:?}"
);
}
}
#[test]
fn plain_strings_and_docstrings_still_offer_sequences() {
for src in ["s = \"\\alph", "\"\"\"\n\\alph"] {
let names = labels(&completions_bare(src, "\\alph"));
assert!(
names.contains(&"\\alpha".to_string()),
"expected sequences in {src:?}, got {names:?}"
);
}
}
#[test]
fn a_lone_escape_stays_quiet_only_inside_a_plain_string() {
assert!(completions_bare("s = \"\\n", "\\n").is_empty());
assert!(completions_bare("s = \"\\u", "\\u").is_empty());
let names = labels(&completions_bare("s = \"\\nu", "\\nu"));
assert!(names.contains(&"\\nu".to_string()), "{names:?}");
assert!(!completions_bare("s = \"\\^", "\\^").is_empty());
assert!(!completions_bare("x = \\n", "\\n").is_empty());
}
#[test]
fn lone_escape_recognition() {
for escape in ["n", "t", "u", "x", "0", "\\", "\""] {
assert!(is_lone_escape(escape), "{escape:?} is an escape");
}
for other in ["^", "_", ":", "alpha", "nu", "", "nn"] {
assert!(!is_lone_escape(other), "{other:?} is not a lone escape");
}
}
#[test]
fn candidate_counts_match_the_repl() {
for (typed, expected) in [
("\\alph", 1),
("\\:smi", 9),
("\\:smile", 4),
("\\:smile:", 1),
("\\:+1:", 1),
] {
let src = format!("x = {typed}");
let items = completions_bare(&src, typed);
assert_eq!(
items.len(),
expected,
"{typed:?} offered {:?}",
labels(&items)
);
}
}
#[test]
fn an_unknown_sequence_offers_nothing() {
assert!(completions_bare("\\notasymbol", "\\notasymbol").is_empty());
}
#[test]
fn prefix_search_takes_exactly_the_matching_run() {
let table: &[(&str, &str)] = &[("\\a", "1"), ("\\ab", "2"), ("\\abc", "3"), ("\\b", "4")];
assert_eq!(prefixed(table, "\\ab").len(), 2);
assert_eq!(prefixed(table, "\\").len(), 4);
assert_eq!(prefixed(table, "\\abcd").len(), 0);
assert_eq!(prefixed(table, "\\z").len(), 0);
}
#[test]
fn codepoints_names_combining_expansions() {
assert_eq!(codepoints("α"), "U+03B1");
assert_eq!(codepoints("⩽̸"), "U+2A7D U+0338");
}
#[test]
fn context_detection() {
assert_eq!(context_at("foo", 3), Context::Value);
assert_eq!(context_at("\\alph", 5), Context::Latex { start: 0 });
assert_eq!(context_at("x = \\_1", 7), Context::Latex { start: 4 });
assert_eq!(context_at("\\", 1), Context::Latex { start: 0 });
assert_eq!(context_at("A \\ b", 5), Context::Value);
let long = format!("\\{}", "a".repeat(MAX_SEQUENCE_LEN));
assert_eq!(context_at(&long, long.len()), Context::Value);
assert_eq!(context_at("@ti", 3), Context::Macro);
assert_eq!(
context_at("Base.", 5),
Context::Member {
receiver: vec!["Base".into()],
macro_member: false,
}
);
assert_eq!(
context_at("A.B.foo", 7),
Context::Member {
receiver: vec!["A".into(), "B".into()],
macro_member: false,
}
);
assert_eq!(
context_at("Base.@ti", 8),
Context::Member {
receiver: vec!["Base".into()],
macro_member: true,
}
);
}
}