дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
Native folding matched comment node names case-sensitively and stripped delimiters character by character, so XML comments were skipped and the `<` in editor-fold tags was consumed. The frontend hardcoded its own comment prefixes. Both now strip the configured comment tokens from the language definitions, so region and editor-fold markers work in block comment languages (XML, HTML, Vue, SVG, CSS and others), and Navigate > Custom Folding lists them. Languages without a bundled grammar get a fallback for own-line markers. Rust and frontend tests share one fixture over 22 language configurations. Fixes #583
1412 рядки
40 KiB
Rust
1412 рядки
40 KiB
Rust
use super::*;
|
|
use crate::FoldKind;
|
|
use ide_core::{OutlineItem, Pos, SymbolKind, Visibility};
|
|
|
|
fn syntax() -> &'static Syntax {
|
|
static S: OnceLock<Syntax> = OnceLock::new();
|
|
S.get_or_init(Syntax::new)
|
|
}
|
|
|
|
fn lang(id: &str) -> LanguageId {
|
|
LanguageId::new(id)
|
|
}
|
|
|
|
/// Renders an outline as indented `Kind name` lines.
|
|
fn render(items: &[OutlineItem]) -> String {
|
|
fn go(items: &[OutlineItem], depth: usize, out: &mut String) {
|
|
for i in items {
|
|
out.push_str(&format!("{:indent$}{:?} {}\n", "", i.kind, i.name, indent = depth * 2));
|
|
go(&i.children, depth + 1, out);
|
|
}
|
|
}
|
|
let mut out = String::new();
|
|
go(items, 0, &mut out);
|
|
out
|
|
}
|
|
|
|
fn check_outline(id: &str, src: &str, expected: &str) {
|
|
let actual = render(&syntax().outline(&lang(id), src));
|
|
let expected = expected.lines().map(str::trim_end).filter(|l| !l.trim().is_empty()).collect::<Vec<_>>();
|
|
let base = expected.iter().map(|l| l.len() - l.trim_start().len()).min().unwrap_or(0);
|
|
let expected: String = expected.iter().map(|l| format!("{}\n", &l[base..])).collect();
|
|
assert_eq!(actual, expected, "outline for {id}:\n{actual}");
|
|
}
|
|
|
|
fn tags(id: &str, src: &str) -> Vec<(String, SymbolKind, Option<String>)> {
|
|
syntax().tags(&lang(id), src).into_iter().map(|t| (t.name, t.kind, t.container)).collect()
|
|
}
|
|
|
|
fn has_tag(
|
|
tags: &[(String, SymbolKind, Option<String>)],
|
|
name: &str,
|
|
kind: SymbolKind,
|
|
container: Option<&str>,
|
|
) -> bool {
|
|
tags.iter().any(|(n, k, c)| n == name && *k == kind && c.as_deref() == container)
|
|
}
|
|
|
|
fn folds(id: &str, src: &str) -> Vec<(u32, u32, FoldKind)> {
|
|
syntax().folding(&lang(id), src).into_iter().map(|f| (f.start_line, f.end_line, f.kind)).collect()
|
|
}
|
|
|
|
#[test]
|
|
fn all_outline_queries_compile() {
|
|
let mut errors = Vec::new();
|
|
for g in &syntax().grammars {
|
|
if let Err(e) = outline::OutlineQuery::new(&g.language, g.spec.outline) {
|
|
errors.push(format!("{}: {e}", g.spec.name));
|
|
}
|
|
}
|
|
assert!(errors.is_empty(), "{}", errors.join("\n"));
|
|
}
|
|
|
|
#[test]
|
|
fn every_grammar_parses_without_abi_errors() {
|
|
for g in &syntax().grammars {
|
|
assert!(parse(&g.language, "x", None).is_some(), "{} failed to parse", g.spec.name);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn syntax_is_send_and_sync() {
|
|
fn assert_send_sync<T: Send + Sync>() {}
|
|
assert_send_sync::<Syntax>();
|
|
}
|
|
|
|
#[test]
|
|
fn supports_resolves_language_ids_and_grammar_names() {
|
|
let s = syntax();
|
|
for id in ["rust", "typescriptreact", "javascriptreact", "shellscript", "jsonc", "terraform", "makefile", "tsx"] {
|
|
assert!(s.supports(&lang(id)), "{id}");
|
|
}
|
|
assert!(!s.supports(&lang("plaintext")));
|
|
assert!(s.outline(&lang("plaintext"), "hello").is_empty());
|
|
assert!(s.folding(&lang("nope"), "a\nb").is_empty());
|
|
assert!(!s.fingerprint().is_empty());
|
|
assert!(s.has_symbols(&lang("rust")));
|
|
assert!(!s.has_symbols(&lang("json")));
|
|
assert!(!s.has_symbols(&lang("markdown")));
|
|
assert!(!s.has_symbols(&lang("plaintext")));
|
|
}
|
|
|
|
#[test]
|
|
fn user_languages_map_onto_bundled_grammars() {
|
|
let cfg: crate::LanguageConfig =
|
|
toml::from_str("id = \"mylang\"\nname = \"Mine\"\ntree-sitter = \"rust\"\n").unwrap();
|
|
let missing: crate::LanguageConfig =
|
|
toml::from_str("id = \"other\"\nname = \"Other\"\ntree-sitter = \"no-such-grammar\"\n").unwrap();
|
|
let s = Syntax::with_languages(&[cfg, missing]);
|
|
assert!(s.supports(&lang("mylang")));
|
|
assert!(!s.supports(&lang("other")));
|
|
assert_eq!(s.tags(&lang("mylang"), "fn a() {}").len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn parallel_use_from_many_threads() {
|
|
let src = "fn a() {}\nstruct B { x: i32 }\n";
|
|
std::thread::scope(|scope| {
|
|
for _ in 0..8 {
|
|
scope.spawn(|| {
|
|
for _ in 0..50 {
|
|
assert_eq!(syntax().tags(&lang("rust"), src).len(), 3);
|
|
assert_eq!(syntax().tags(&lang("python"), "def f():\n pass\n").len(), 1);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// ---- outlines per language ----
|
|
|
|
#[test]
|
|
fn outline_rust() {
|
|
let src = r#"
|
|
use std::fmt;
|
|
|
|
pub struct Foo<T> {
|
|
a: i32,
|
|
pub b: T,
|
|
}
|
|
|
|
enum E { A, B(i32) }
|
|
|
|
trait Tr {
|
|
fn req(&self);
|
|
fn prov(&self) {}
|
|
}
|
|
|
|
impl<T> Foo<T> {
|
|
pub fn new() -> Self { todo!() }
|
|
fn inner() {
|
|
fn nested() {}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Foo<i32> {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Ok(()) }
|
|
}
|
|
|
|
mod m {
|
|
pub const K: u32 = 1;
|
|
macro_rules! mac { () => {} }
|
|
type Alias = u8;
|
|
}
|
|
|
|
fn main() {}
|
|
"#;
|
|
check_outline(
|
|
"rust",
|
|
src,
|
|
"
|
|
Struct Foo
|
|
Field a
|
|
Field b
|
|
Enum E
|
|
EnumMember A
|
|
EnumMember B
|
|
Trait Tr
|
|
Method req
|
|
Method prov
|
|
Namespace impl Foo<T>
|
|
Method new
|
|
Method inner
|
|
Function nested
|
|
Namespace impl fmt::Display for Foo<i32>
|
|
Method fmt
|
|
Module m
|
|
Constant K
|
|
Macro mac
|
|
TypeAlias Alias
|
|
Function main
|
|
",
|
|
);
|
|
let t = tags("rust", src);
|
|
assert!(has_tag(&t, "new", SymbolKind::Method, Some("Foo")));
|
|
assert!(has_tag(&t, "fmt", SymbolKind::Method, Some("Foo")));
|
|
assert!(has_tag(&t, "nested", SymbolKind::Function, Some("inner")));
|
|
assert!(has_tag(&t, "K", SymbolKind::Constant, Some("m")));
|
|
assert!(!t.iter().any(|(n, _, _)| n.starts_with("impl")), "impl blocks are not symbols");
|
|
}
|
|
|
|
#[test]
|
|
fn outline_ranges_are_utf16() {
|
|
let src = "/* 😀 é */ fn café() {}\n";
|
|
let items = syntax().outline(&lang("rust"), src);
|
|
assert_eq!(items.len(), 1);
|
|
let item = &items[0];
|
|
assert_eq!(item.name, "café");
|
|
// "/* 😀 é */ " is 11 UTF-16 units (the emoji counts twice).
|
|
assert_eq!(item.range.start, Pos::new(0, 11));
|
|
assert_eq!(item.selection_range.start, Pos::new(0, 14));
|
|
assert_eq!(item.selection_range.end, Pos::new(0, 18));
|
|
assert_eq!(item.range.end, Pos::new(0, 23));
|
|
}
|
|
|
|
#[test]
|
|
fn outline_typescript() {
|
|
let src = r#"
|
|
import { a } from "a";
|
|
export interface Shape {
|
|
area(): number;
|
|
name: string;
|
|
}
|
|
export class Circle implements Shape {
|
|
private r = 1;
|
|
constructor(r: number) { this.r = r; }
|
|
area(): number { return 3 * this.r; }
|
|
handler = () => {};
|
|
}
|
|
enum Color { Red, Green = 2 }
|
|
type Id = string;
|
|
namespace NS { export function f() {} }
|
|
export function top(x: number): void {}
|
|
export const arrow = (y: string) => y;
|
|
const K = 1;
|
|
let v = 2;
|
|
"#;
|
|
check_outline(
|
|
"typescript",
|
|
src,
|
|
"
|
|
Interface Shape
|
|
Method area
|
|
Property name
|
|
Class Circle
|
|
Field r
|
|
Constructor constructor
|
|
Method area
|
|
Method handler
|
|
Enum Color
|
|
EnumMember Red
|
|
EnumMember Green
|
|
TypeAlias Id
|
|
Namespace NS
|
|
Function f
|
|
Function top
|
|
Function arrow
|
|
Constant K
|
|
Variable v
|
|
",
|
|
);
|
|
let t = tags("typescript", src);
|
|
assert!(has_tag(&t, "area", SymbolKind::Method, Some("Circle")));
|
|
assert!(has_tag(&t, "Circle", SymbolKind::Class, None));
|
|
}
|
|
|
|
#[test]
|
|
fn outline_tsx_and_jsx() {
|
|
let src = "export function App() {\n return <div className=\"x\">\n <Child />\n </div>;\n}\nclass Widget extends React.Component { render() { return <span/>; } }\n";
|
|
let expected = "
|
|
Function App
|
|
Class Widget
|
|
Method render
|
|
";
|
|
check_outline("typescriptreact", src, expected);
|
|
check_outline("javascriptreact", src, expected);
|
|
assert!(folds("typescriptreact", src).contains(&(0, 4, FoldKind::Region)));
|
|
}
|
|
|
|
#[test]
|
|
fn outline_javascript() {
|
|
check_outline(
|
|
"javascript",
|
|
r#"
|
|
class A extends B {
|
|
#priv = 1;
|
|
constructor() { super(); }
|
|
static s() {}
|
|
}
|
|
function f(a, b) {}
|
|
function* gen() {}
|
|
const g = () => {};
|
|
module.exports.k = function () {};
|
|
var old = 1;
|
|
"#,
|
|
"
|
|
Class A
|
|
Field #priv
|
|
Constructor constructor
|
|
Method s
|
|
Function f
|
|
Function gen
|
|
Function g
|
|
Function k
|
|
Variable old
|
|
",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn outline_python() {
|
|
let src = r#"
|
|
import os
|
|
|
|
X = 1
|
|
|
|
class Foo(Base):
|
|
attr = 2
|
|
|
|
def __init__(self, a):
|
|
self.a = a
|
|
|
|
@property
|
|
def p(self):
|
|
return 1
|
|
|
|
class Inner:
|
|
pass
|
|
|
|
def top(a, b=1):
|
|
def nested():
|
|
pass
|
|
return nested
|
|
"#;
|
|
check_outline(
|
|
"python",
|
|
src,
|
|
"
|
|
Variable X
|
|
Class Foo
|
|
Field attr
|
|
Constructor __init__
|
|
Method p
|
|
Class Inner
|
|
Function top
|
|
Function nested
|
|
",
|
|
);
|
|
let t = tags("python", src);
|
|
assert!(has_tag(&t, "p", SymbolKind::Method, Some("Foo")));
|
|
assert!(has_tag(&t, "Inner", SymbolKind::Class, Some("Foo")));
|
|
}
|
|
|
|
#[test]
|
|
fn outline_go() {
|
|
let src = r#"package main
|
|
|
|
import (
|
|
"fmt"
|
|
)
|
|
|
|
type Server struct {
|
|
Addr string
|
|
}
|
|
|
|
type Handler interface {
|
|
Serve(w int) error
|
|
}
|
|
|
|
func (s *Server) Start() error { return nil }
|
|
|
|
func main() {}
|
|
|
|
const Version = "1"
|
|
"#;
|
|
check_outline(
|
|
"go",
|
|
src,
|
|
"
|
|
Package main
|
|
Struct Server
|
|
Field Addr
|
|
Interface Handler
|
|
Method Serve
|
|
Method Start
|
|
Function main
|
|
Constant Version
|
|
",
|
|
);
|
|
let t = tags("go", src);
|
|
assert!(has_tag(&t, "Start", SymbolKind::Method, Some("Server")));
|
|
assert!(has_tag(&t, "main", SymbolKind::Function, None));
|
|
}
|
|
|
|
#[test]
|
|
fn outline_java() {
|
|
check_outline(
|
|
"java",
|
|
r#"
|
|
package com.x;
|
|
import java.util.List;
|
|
public class A extends B {
|
|
private static final int K = 1;
|
|
public A(String n) { }
|
|
public void go(int x) {}
|
|
enum Inner { X, Y }
|
|
interface I { void m(); }
|
|
record R(int a) {}
|
|
}
|
|
"#,
|
|
"
|
|
Class A
|
|
Field K
|
|
Constructor A
|
|
Method go
|
|
Enum Inner
|
|
EnumMember X
|
|
EnumMember Y
|
|
Interface I
|
|
Method m
|
|
Class R
|
|
",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn outline_kotlin() {
|
|
check_outline(
|
|
"kotlin",
|
|
r#"package com.example
|
|
|
|
import a.b.C
|
|
|
|
class Foo(val x: Int) : Bar {
|
|
val y = 1
|
|
fun go(a: Int): Int {
|
|
return a
|
|
}
|
|
companion object {
|
|
const val K = 1
|
|
}
|
|
}
|
|
|
|
object Single {
|
|
fun f() {}
|
|
}
|
|
|
|
interface I {
|
|
fun m()
|
|
}
|
|
|
|
enum class Color {
|
|
RED, GREEN
|
|
}
|
|
|
|
fun top() = 1
|
|
"#,
|
|
"
|
|
Class Foo
|
|
Property y
|
|
Method go
|
|
Class companion
|
|
Property K
|
|
Class Single
|
|
Method f
|
|
Interface I
|
|
Method m
|
|
Enum Color
|
|
EnumMember RED
|
|
EnumMember GREEN
|
|
Function top
|
|
",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn outline_scala() {
|
|
check_outline(
|
|
"scala",
|
|
r#"package foo
|
|
class Foo(x: Int) extends Bar {
|
|
val y = 1
|
|
def go(a: Int): Int = a
|
|
}
|
|
object Single { def f(): Unit = () }
|
|
trait Tr { def m(): Int }
|
|
enum Color { case Red, Green }
|
|
"#,
|
|
"
|
|
Package foo
|
|
Class Foo
|
|
Field y
|
|
Method go
|
|
Class Single
|
|
Method f
|
|
Trait Tr
|
|
Method m
|
|
Enum Color
|
|
EnumMember Red
|
|
EnumMember Green
|
|
",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn outline_c() {
|
|
let src = "#include <stdio.h>\n#define MAX 10\nstruct point { int x; int *y; };\ntypedef struct point point_t;\nenum color { RED };\nint add(int a, int b);\nint add(int a, int b) {\n return a + b;\n}\nchar *name(void) { return 0; }\n";
|
|
check_outline(
|
|
"c",
|
|
src,
|
|
"
|
|
Macro MAX
|
|
Struct point
|
|
Field x
|
|
Field y
|
|
TypeAlias point_t
|
|
Enum color
|
|
EnumMember RED
|
|
Function add
|
|
Function add
|
|
Function name
|
|
",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn outline_cpp() {
|
|
let src = r#"#include <vector>
|
|
namespace app {
|
|
class Widget : public Base {
|
|
public:
|
|
Widget();
|
|
void draw() const;
|
|
int size;
|
|
};
|
|
Widget::Widget() {}
|
|
void Widget::draw() const {}
|
|
}
|
|
int main() { return 0; }
|
|
"#;
|
|
check_outline(
|
|
"cpp",
|
|
src,
|
|
"
|
|
Namespace app
|
|
Class Widget
|
|
Constructor Widget
|
|
Method draw
|
|
Field size
|
|
Constructor Widget
|
|
Method draw
|
|
Function main
|
|
",
|
|
);
|
|
let t = tags("cpp", src);
|
|
assert!(has_tag(&t, "draw", SymbolKind::Method, Some("Widget")));
|
|
assert_eq!(t.iter().filter(|(n, k, _)| n == "draw" && *k == SymbolKind::Method).count(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn outline_csharp() {
|
|
check_outline(
|
|
"csharp",
|
|
r#"using System;
|
|
namespace Foo.Bar {
|
|
public class C : B {
|
|
private int x;
|
|
public const int K = 1;
|
|
public int P { get; set; }
|
|
public C() {}
|
|
public void Go(int a) {}
|
|
}
|
|
interface I { void M(); }
|
|
enum E { A }
|
|
}
|
|
"#,
|
|
"
|
|
Namespace Foo.Bar
|
|
Class C
|
|
Field x
|
|
Constant K
|
|
Property P
|
|
Constructor C
|
|
Method Go
|
|
Interface I
|
|
Method M
|
|
Enum E
|
|
EnumMember A
|
|
",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn outline_swift() {
|
|
check_outline(
|
|
"swift",
|
|
r#"import Foundation
|
|
class Foo: Bar {
|
|
var x: Int = 1
|
|
init(x: Int) { self.x = x }
|
|
func go(a: Int) -> Int { return a }
|
|
}
|
|
struct S { let y: Int }
|
|
protocol P { func m() }
|
|
extension Foo: P { func m() {} }
|
|
func top() {}
|
|
"#,
|
|
"
|
|
Class Foo
|
|
Property x
|
|
Constructor init
|
|
Method go
|
|
Struct S
|
|
Property y
|
|
Interface P
|
|
Method m
|
|
Namespace Foo
|
|
Method m
|
|
Function top
|
|
",
|
|
);
|
|
let t = tags("swift", "extension Foo { func m() {} }\n");
|
|
assert!(has_tag(&t, "m", SymbolKind::Method, Some("Foo")));
|
|
}
|
|
|
|
#[test]
|
|
fn outline_ruby() {
|
|
check_outline(
|
|
"ruby",
|
|
r#"module Billing
|
|
class Invoice < Base
|
|
attr_accessor :total
|
|
VERSION = "1"
|
|
def initialize(total)
|
|
@total = total
|
|
end
|
|
def self.build
|
|
end
|
|
end
|
|
end
|
|
"#,
|
|
"
|
|
Module Billing
|
|
Class Invoice
|
|
Property total
|
|
Constant VERSION
|
|
Constructor initialize
|
|
Method build
|
|
",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn outline_php() {
|
|
check_outline(
|
|
"php",
|
|
r#"<?php
|
|
namespace App;
|
|
class Foo extends Bar {
|
|
const K = 1;
|
|
private int $x = 1;
|
|
public function __construct() {}
|
|
public function go(int $a): int { return $a; }
|
|
}
|
|
interface I { function m(); }
|
|
trait T { function t() {} }
|
|
function top() {}
|
|
"#,
|
|
"
|
|
Namespace App
|
|
Class Foo
|
|
Constant K
|
|
Property $x
|
|
Constructor __construct
|
|
Method go
|
|
Interface I
|
|
Method m
|
|
Trait T
|
|
Method t
|
|
Function top
|
|
",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn outline_lua() {
|
|
let src =
|
|
"local M = {}\nfunction M.go(a) return a end\nfunction M:method() end\nlocal function priv() end\nreturn M\n";
|
|
check_outline(
|
|
"lua",
|
|
src,
|
|
"
|
|
Variable M
|
|
Function go
|
|
Method method
|
|
Function priv
|
|
",
|
|
);
|
|
let t = tags("lua", src);
|
|
assert!(has_tag(&t, "go", SymbolKind::Function, Some("M")));
|
|
}
|
|
|
|
#[test]
|
|
fn outline_bash() {
|
|
check_outline(
|
|
"shellscript",
|
|
"#!/bin/bash\nNAME=foo\nbuild() {\n echo hi\n}\nfunction deploy {\n echo x\n}\n",
|
|
"
|
|
Variable NAME
|
|
Function build
|
|
Function deploy
|
|
",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn outline_data_formats_are_outline_only() {
|
|
let json = "{\n \"name\": \"x\",\n \"scripts\": {\n \"build\": \"y\"\n }\n}\n";
|
|
check_outline(
|
|
"json",
|
|
json,
|
|
"
|
|
Key name
|
|
Key scripts
|
|
Key build
|
|
",
|
|
);
|
|
assert!(tags("json", json).is_empty());
|
|
check_outline(
|
|
"yaml",
|
|
"on:\n push:\n branches: [main]\njobs:\n build: {}\n",
|
|
"
|
|
Key on
|
|
Key push
|
|
Key branches
|
|
Key jobs
|
|
Key build
|
|
",
|
|
);
|
|
check_outline(
|
|
"toml",
|
|
"[package]\nname = \"x\"\n\n[[bin]]\nname = \"b\"\n",
|
|
"
|
|
Module package
|
|
Key name
|
|
Module bin
|
|
Key name
|
|
",
|
|
);
|
|
check_outline(
|
|
"markdown",
|
|
"# Title\n\nIntro\n\n## A\n\ntext\n\n### Sub\n\n## B\n",
|
|
"
|
|
Key Title
|
|
Key A
|
|
Key Sub
|
|
Key B
|
|
",
|
|
);
|
|
assert!(tags("markdown", "# Title\n").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn outline_html_css_xml() {
|
|
check_outline(
|
|
"html",
|
|
"<html>\n<body>\n <div id=\"main\"><p>hi</p></div>\n <script>let a = 1;</script>\n</body>\n</html>\n",
|
|
"
|
|
Field html
|
|
Field body
|
|
Field div
|
|
Field script
|
|
",
|
|
);
|
|
check_outline(
|
|
"css",
|
|
":root {\n --accent: #fff;\n}\n.a, .b > c { color: red; }\n@media (max-width: 10px) {\n .x { y: 1 }\n}\n",
|
|
"
|
|
Class :root
|
|
Variable --accent
|
|
Class .a, .b > c
|
|
Namespace @media
|
|
Class .x
|
|
",
|
|
);
|
|
check_outline(
|
|
"xml",
|
|
"<?xml version=\"1.0\"?>\n<project>\n <dependencies>\n <dependency/>\n </dependencies>\n</project>\n",
|
|
"
|
|
Field project
|
|
Field dependencies
|
|
Field dependency
|
|
",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn outline_more_languages() {
|
|
check_outline(
|
|
"dart",
|
|
"class Foo {\n int x = 1;\n Foo(this.x);\n int go(int a) { return a; }\n}\nvoid top() {}\n",
|
|
"
|
|
Class Foo
|
|
Field x
|
|
Constructor Foo
|
|
Method go
|
|
Function top
|
|
",
|
|
);
|
|
check_outline(
|
|
"zig",
|
|
"pub const Foo = struct {\n x: i32,\n pub fn go(self: Foo) i32 { return self.x; }\n};\nfn top() void {}\ntest \"basic\" {}\n",
|
|
"
|
|
Struct Foo
|
|
Field x
|
|
Method go
|
|
Function top
|
|
Function basic
|
|
",
|
|
);
|
|
check_outline(
|
|
"elixir",
|
|
"defmodule Foo.Bar do\n def go(a), do: a\n defp priv(b) do\n b\n end\n defmacro m(x), do: x\nend\n",
|
|
"
|
|
Module Foo.Bar
|
|
Function go
|
|
Function priv
|
|
Macro m
|
|
",
|
|
);
|
|
check_outline(
|
|
"haskell",
|
|
"module Foo where\ndata Shape = Circle Int | Square\nclass C a where\n m :: a -> Int\ngo :: Int -> Int\ngo a = a\n",
|
|
"
|
|
Module Foo
|
|
Enum Shape
|
|
EnumMember Circle
|
|
EnumMember Square
|
|
Interface C
|
|
Method m
|
|
Function go
|
|
",
|
|
);
|
|
check_outline(
|
|
"ocaml",
|
|
"type t = { a : int }\nlet go x = x\nmodule M = struct\n let inner y = y\nend\n",
|
|
"
|
|
Struct t
|
|
Field a
|
|
Function go
|
|
Module M
|
|
Function inner
|
|
",
|
|
);
|
|
check_outline(
|
|
"ocaml.interface",
|
|
"type t\nval go : int -> int\nmodule type S = sig val f : int -> int end\n",
|
|
"
|
|
TypeAlias t
|
|
Function go
|
|
Interface S
|
|
Method f
|
|
",
|
|
);
|
|
check_outline(
|
|
"terraform",
|
|
"variable \"region\" {\n default = \"us\"\n}\nresource \"aws_instance\" \"web\" {\n ami = \"x\"\n}\n",
|
|
"
|
|
Struct variable.region
|
|
Property default
|
|
Struct resource.aws_instance.web
|
|
Property ami
|
|
",
|
|
);
|
|
check_outline(
|
|
"proto",
|
|
"syntax = \"proto3\";\npackage foo.v1;\nmessage Foo {\n int32 a = 1;\n enum Kind { K0 = 0; }\n}\nservice Svc {\n rpc Get(Foo) returns (Foo);\n}\n",
|
|
"
|
|
Package foo.v1
|
|
Struct Foo
|
|
Field a
|
|
Enum Kind
|
|
EnumMember K0
|
|
Interface Svc
|
|
Method Get
|
|
",
|
|
);
|
|
check_outline(
|
|
"graphql",
|
|
"type Query {\n user(id: ID!): User\n}\nenum Role { ADMIN }\nquery GetUser { user(id: 1) { id } }\n",
|
|
"
|
|
Class Query
|
|
Field user
|
|
Enum Role
|
|
EnumMember ADMIN
|
|
Function GetUser
|
|
",
|
|
);
|
|
check_outline(
|
|
"nix",
|
|
"{ pkgs, ... }:\nlet\n foo = 1;\n bar = x: x;\nin { inherit foo; }\n",
|
|
"
|
|
Property foo
|
|
Function bar
|
|
",
|
|
);
|
|
check_outline(
|
|
"makefile",
|
|
"CC := gcc\nall: build\nbuild: main.o\n\t$(CC) -o app main.o\n",
|
|
"
|
|
Variable CC
|
|
Function all
|
|
Function build
|
|
",
|
|
);
|
|
check_outline(
|
|
"gdscript",
|
|
"class_name Player\nextends Node\n\nsignal died\nenum State { IDLE, RUN }\nconst SPEED = 2\n@export var hp := 3\n\nfunc _init(a):\n\tpass\n\nfunc jump(height: float) -> void:\n\tvar local = 1\n\nclass Inner:\n\tvar x = 0\n\tfunc go():\n\t\tpass\n",
|
|
"
|
|
Class Player
|
|
Event died
|
|
Enum State
|
|
EnumMember IDLE
|
|
EnumMember RUN
|
|
Constant SPEED
|
|
Field hp
|
|
Constructor _init
|
|
Function jump
|
|
Class Inner
|
|
Field x
|
|
Method go
|
|
",
|
|
);
|
|
check_outline(
|
|
"cmake",
|
|
"project(Foo)\nfunction(my_fn arg)\n message(${arg})\nendfunction()\nadd_executable(app main.c)\n",
|
|
"
|
|
Module Foo
|
|
Function my_fn
|
|
Class app
|
|
",
|
|
);
|
|
}
|
|
|
|
// ---- folding ----
|
|
|
|
#[test]
|
|
fn folding_rust() {
|
|
let src = r#"use a;
|
|
use b::{
|
|
c,
|
|
};
|
|
|
|
// one
|
|
// two
|
|
// three
|
|
fn f() {
|
|
let v = vec![
|
|
1,
|
|
];
|
|
}
|
|
|
|
/* block
|
|
comment */
|
|
// region Helpers
|
|
fn g() {}
|
|
// endregion
|
|
"#;
|
|
assert_eq!(
|
|
folds("rust", src),
|
|
vec![
|
|
(0, 3, FoldKind::Imports),
|
|
(1, 3, FoldKind::Region),
|
|
(5, 7, FoldKind::Comment),
|
|
(8, 12, FoldKind::Region),
|
|
(9, 11, FoldKind::Region),
|
|
(14, 15, FoldKind::Comment),
|
|
(16, 18, FoldKind::Region),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn folding_xml_custom_regions() {
|
|
for (start, end) in [
|
|
("<!--region要折叠的内容-->", "<!-- endregion -->"),
|
|
("<!--region name-->", "<!--endregion-->"),
|
|
("<!-- <editor-fold desc=\"Head\"> -->", "<!-- </editor-fold> -->"),
|
|
] {
|
|
let text = format!("<project>\n {start}\n <name>demo</name>\n {end}\n</project>\n");
|
|
assert!(folds("xml", &text).contains(&(1, 3, FoldKind::Region)), "{text}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn folding_custom_regions_use_configured_comment_tokens() {
|
|
let fixture: serde_json::Value =
|
|
serde_json::from_str(include_str!("../../../../fixtures/folding/regions.json")).unwrap();
|
|
let registry = LanguageRegistry::load(None).unwrap();
|
|
let cache = SyntaxCache::new(std::sync::Arc::new(Syntax::new()));
|
|
for language in fixture["languages"].as_array().unwrap() {
|
|
let id = language.as_str().unwrap();
|
|
let cfg = registry.get(&lang(id)).unwrap();
|
|
let mut tokens = Vec::new();
|
|
if let Some(line) = &cfg.line_comment {
|
|
tokens.push((line.as_str(), ""));
|
|
}
|
|
if let Some((open, close)) = &cfg.block_comment {
|
|
tokens.push((open.as_str(), close.as_str()));
|
|
}
|
|
for (open, close) in tokens {
|
|
for space in ["", " ", " "] {
|
|
// Word delimiters require a separator in the language itself.
|
|
if space.is_empty() && open.ends_with(|c: char| c.is_ascii_alphanumeric()) {
|
|
continue;
|
|
}
|
|
let wrap = |body: &str| format!(" {open}{space}{body}{space}{close}");
|
|
for marker in fixture["markers"].as_array().unwrap() {
|
|
let start = wrap(marker["start"].as_str().unwrap());
|
|
let end = wrap(marker["end"].as_str().unwrap());
|
|
let tokens = folding::CommentTokens::from(&cfg);
|
|
assert_eq!(folding::region_marker(&start, &tokens), Some(true), "{id}: {start}");
|
|
assert_eq!(folding::region_marker(&end, &tokens), Some(false), "{id}: {end}");
|
|
// Ruby block delimiters must occupy separate lines; Haskell
|
|
// interprets `--` followed immediately by a symbol as an operator.
|
|
if (id == "ruby" && open == "=begin") || (id == "haskell" && space.is_empty()) {
|
|
continue;
|
|
}
|
|
let text = format!("{start}\n\n{end}\n");
|
|
let expected = vec![(0, 2, FoldKind::Region)];
|
|
assert_eq!(folds(id, &text), expected, "{id}: {text}");
|
|
let doc = ide_core::DocId::next();
|
|
cache.open(doc, &lang(id), 0);
|
|
let actual = cache.folding(doc, &lang(id), 0, &text);
|
|
assert_eq!(cache.folding(doc, &lang(id), 0, &text), actual);
|
|
cache.close(doc);
|
|
assert_eq!(
|
|
actual.iter().map(|f| (f.start_line, f.end_line, f.kind)).collect::<Vec<_>>(),
|
|
expected,
|
|
"cached {id}: {text}"
|
|
);
|
|
let nested = format!("{end}\n\n{start}\n{start}\n\n{end}\n{end}\n{start}\n");
|
|
assert_eq!(
|
|
folds(id, &nested),
|
|
vec![(2, 6, FoldKind::Region), (3, 5, FoldKind::Region)],
|
|
"nested {id}: {nested}"
|
|
);
|
|
}
|
|
for invalid in fixture["invalid"].as_array().unwrap() {
|
|
let start = wrap(invalid.as_str().unwrap());
|
|
assert_eq!(
|
|
folding::region_marker(&start, &folding::CommentTokens::from(&cfg)),
|
|
None,
|
|
"invalid {id}: {start}"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn folding_python_bodies_start_at_header() {
|
|
let src = "import os\nimport sys\n\nclass A:\n def f(self):\n return [\n 1,\n ]\n\n\ndef g():\n pass\n";
|
|
assert_eq!(
|
|
folds("python", src),
|
|
vec![
|
|
(0, 1, FoldKind::Imports),
|
|
(3, 7, FoldKind::Region),
|
|
(4, 7, FoldKind::Region),
|
|
(5, 7, FoldKind::Region),
|
|
(10, 11, FoldKind::Region),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn folding_data_and_markup() {
|
|
assert_eq!(
|
|
folds("json", "{\n \"a\": [\n 1\n ],\n \"b\": {}\n}\n"),
|
|
vec![(0, 5, FoldKind::Region), (1, 3, FoldKind::Region)]
|
|
);
|
|
assert_eq!(
|
|
folds("yaml", "a:\n b: 1\n c:\n - x\n - y\nd: 2\n"),
|
|
vec![(0, 4, FoldKind::Region), (2, 4, FoldKind::Region)]
|
|
);
|
|
assert_eq!(
|
|
folds("html", "<div>\n <!-- a\n b -->\n <p>x</p>\n</div>\n"),
|
|
vec![(0, 4, FoldKind::Region), (1, 2, FoldKind::Comment)]
|
|
);
|
|
assert_eq!(
|
|
folds("markdown", "# A\n\ntext\n\n```rust\nfn x() {}\n```\n\n# B\n\nmore\n"),
|
|
vec![(0, 6, FoldKind::Region), (4, 6, FoldKind::Region), (8, 10, FoldKind::Region)]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn folding_go_imports_and_ruby_ends() {
|
|
assert_eq!(
|
|
folds("go", "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n)\n\nfunc main() {\n}\n"),
|
|
vec![(2, 5, FoldKind::Imports), (7, 8, FoldKind::Region)]
|
|
);
|
|
assert_eq!(
|
|
folds("ruby", "class A\n def f\n 1\n end\nend\n"),
|
|
vec![(0, 4, FoldKind::Region), (1, 3, FoldKind::Region)]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn folding_swift_control_flow_and_haskell_binds() {
|
|
assert_eq!(
|
|
folds("swift", "func f() {\n if x {\n g()\n }\n for i in xs {\n g()\n }\n}\n"),
|
|
vec![(0, 7, FoldKind::Region), (1, 3, FoldKind::Region), (4, 6, FoldKind::Region)]
|
|
);
|
|
assert_eq!(
|
|
folds("haskell", "main :: IO ()\nmain = do\n putStrLn \"a\"\n putStrLn \"b\"\n"),
|
|
vec![(1, 3, FoldKind::Region)]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn folds_are_unique_per_start_line_and_sorted() {
|
|
for (id, src) in [
|
|
("typescript", "foo(function () {\n bar([\n 1,\n ]);\n});\n"),
|
|
("java", "class A {\n void f() {\n if (x) {\n } else {\n }\n }\n}\n"),
|
|
] {
|
|
let f = folds(id, src);
|
|
assert!(f.windows(2).all(|w| w[0].0 < w[1].0), "{id}: {f:?}");
|
|
assert!(f.iter().all(|(s, e, _)| s < e));
|
|
}
|
|
}
|
|
|
|
// ---- selection ranges ----
|
|
|
|
fn sel_texts(id: &str, src: &str, caret: &str) -> Vec<String> {
|
|
let at = src.find(caret).expect("caret marker");
|
|
let src = src.replacen(caret, "", 1);
|
|
let offset = src[..at].encode_utf16().count() as u32;
|
|
let lines: Vec<&str> = src.split('\n').collect();
|
|
let to_byte = |p: Pos| {
|
|
let line_start: usize = lines[..p.line as usize].iter().map(|l| l.len() + 1).sum();
|
|
let line = lines[p.line as usize];
|
|
line_start + crate::syntax::lines::utf16_to_byte(line, p.col as usize)
|
|
};
|
|
syntax()
|
|
.selection_ranges(&lang(id), &src, offset)
|
|
.into_iter()
|
|
.map(|r| src[to_byte(r.start)..to_byte(r.end)].to_owned())
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn selection_ranges_rust_grow_outwards() {
|
|
let src = "fn main() {\n let total = compute(a + b|, c);\n}\n";
|
|
let ranges = sel_texts("rust", src, "|");
|
|
assert_eq!(ranges[0], "b");
|
|
assert_eq!(ranges[1], "a + b");
|
|
assert_eq!(ranges[2], "a + b, c");
|
|
assert_eq!(ranges[3], "(a + b, c)");
|
|
assert_eq!(ranges[4], "compute(a + b, c)");
|
|
assert!(ranges.contains(&"let total = compute(a + b, c);".to_owned()));
|
|
assert!(ranges.contains(&"{\n let total = compute(a + b, c);\n}".to_owned()));
|
|
assert_eq!(ranges.last().unwrap(), &src.replacen('|', "", 1));
|
|
// Strictly growing.
|
|
assert!(ranges.windows(2).all(|w| w[0].len() < w[1].len()));
|
|
}
|
|
|
|
#[test]
|
|
fn selection_ranges_words_inside_comments_and_strings() {
|
|
let ranges = sel_texts("python", "x = \"hello wo|rld\"\n", "|");
|
|
assert_eq!(&ranges[..3], &["world".to_owned(), "hello world".to_owned(), "\"hello world\"".to_owned()]);
|
|
let ranges = sel_texts("rust", "// some com|ment text\nfn a() {}\n", "|");
|
|
assert_eq!(ranges[0], "comment");
|
|
assert_eq!(ranges[1], "// some comment text");
|
|
}
|
|
|
|
#[test]
|
|
fn selection_ranges_utf16_and_fallback() {
|
|
let ranges = sel_texts("rust", "fn f() { let s = \"😀\"; let ab|c = 1; }\n", "|");
|
|
assert_eq!(ranges[0], "abc");
|
|
let ranges = sel_texts("plaintext", "one two\n three fo|ur \nfive\n", "|");
|
|
assert_eq!(ranges, vec!["four", "three four", " three four ", "one two\n three four \nfive\n"]);
|
|
}
|
|
|
|
#[test]
|
|
fn caret_after_identifier_selects_it() {
|
|
let ranges = sel_texts("typescript", "foo(bar|);\n", "|");
|
|
assert_eq!(ranges[0], "bar");
|
|
}
|
|
|
|
#[test]
|
|
fn large_input_is_not_parsed() {
|
|
let big = "a".repeat(MAX_PARSE_BYTES + 1);
|
|
assert!(syntax().outline(&lang("rust"), &big).is_empty());
|
|
}
|
|
|
|
// --- web references -------------------------------------------------------------
|
|
|
|
fn web_defs(id: &str, src: &str) -> Vec<(String, SymbolKind, u32, u32)> {
|
|
syntax()
|
|
.web_definitions(&lang(id), src)
|
|
.into_iter()
|
|
.map(|t| (t.name, t.kind, t.range.start.line, t.range.start.col))
|
|
.collect()
|
|
}
|
|
|
|
fn web_ref(id: &str, src: &str) -> Option<(WebReferenceKind, String, u32, u32)> {
|
|
let at = src.find('|').expect("caret marker");
|
|
let text = src.replace('|', "");
|
|
let line = text[..at].matches('\n').count() as u32;
|
|
let col = (at - text[..at].rfind('\n').map_or(0, |i| i + 1)) as u32;
|
|
syntax()
|
|
.web_reference_at(&lang(id), &text, Pos::new(line, col))
|
|
.map(|r| (r.kind, r.name, r.range.start.col, r.range.end.col))
|
|
}
|
|
|
|
#[test]
|
|
fn web_definitions_of_stylesheets_and_style_elements() {
|
|
let css = ".a.b, div#main > .c:hover { color: var(--x); --y: 1px; }\n@media (x) { .d {} }\n";
|
|
assert_eq!(
|
|
web_defs("css", css),
|
|
vec![
|
|
(".a".into(), CSS_CLASS, 0, 1),
|
|
(".b".into(), CSS_CLASS, 0, 3),
|
|
("#main".into(), CSS_ID, 0, 10),
|
|
(".c".into(), CSS_CLASS, 0, 18),
|
|
("--y".into(), CSS_VARIABLE, 0, 45),
|
|
(".d".into(), CSS_CLASS, 1, 14),
|
|
]
|
|
);
|
|
// Pseudo-classes (`:hover`) are not classes; nested SCSS rules parse with the CSS grammar.
|
|
assert!(!web_defs("css", css).iter().any(|d| d.0 == ".hover"));
|
|
assert!(web_defs("scss", ".card { &:hover { } .title { } }").iter().any(|d| d.0 == ".title"));
|
|
let html = "<p class=\"x\">\n<style>\n .card { --gap: 2px }\n</style>";
|
|
assert_eq!(web_defs("html", html), vec![(".card".into(), CSS_CLASS, 2, 3), ("--gap".into(), CSS_VARIABLE, 2, 10)]);
|
|
assert!(syntax().tags(&lang("css"), css).iter().any(|t| t.name == ".a"));
|
|
assert!(syntax().has_symbols(&lang("css")) && syntax().has_symbols(&lang("vue")));
|
|
}
|
|
|
|
#[test]
|
|
fn web_references_in_attributes_and_stylesheets() {
|
|
use WebReferenceKind::*;
|
|
assert_eq!(web_ref("html", r##"<div class="card bi|g">"##), Some((Class, "big".into(), 17, 20)));
|
|
assert_eq!(web_ref("html", r##"<div class="|card big">"##), Some((Class, "card".into(), 12, 16)));
|
|
assert_eq!(web_ref("html", r##"<div id="ma|in">"##), Some((Id, "main".into(), 9, 13)));
|
|
assert_eq!(web_ref("html", r##"<a href="#to|p">"##), Some((Anchor, "top".into(), 9, 13)));
|
|
assert_eq!(web_ref("html", r##"<a href="page.ht|ml">"##), None);
|
|
assert_eq!(
|
|
web_ref("html", r##"<p style="color: var(--ac|cent)">"##),
|
|
Some((CssVariable, "--accent".into(), 21, 29))
|
|
);
|
|
assert_eq!(web_ref("html", "<style>\n.a { color: var(--x|) }\n</style>").map(|r| r.1), Some("--x".into()));
|
|
assert_eq!(web_ref("css", ".a { color: var(--a|ccent, red) }"), Some((CssVariable, "--accent".into(), 16, 24)));
|
|
assert_eq!(web_ref("css", ".a { color: re|d }"), None);
|
|
assert_eq!(web_ref("vue", r##"<template><div class="ti|tle"/></template>"##).map(|r| r.0), Some(Class));
|
|
}
|
|
|
|
#[test]
|
|
fn web_anchors_find_ids_and_names() {
|
|
let html = "<h2 id=\"top\">x</h2>\n<a name=\"top\"></a>\n<p class=\"top\">";
|
|
let found: Vec<(u32, u32)> =
|
|
syntax().web_anchors(&lang("html"), html, "top").into_iter().map(|r| (r.start.line, r.start.col)).collect();
|
|
assert_eq!(found, vec![(0, 8), (1, 9)]);
|
|
}
|
|
|
|
#[test]
|
|
fn web_file_references_in_attributes_and_stylesheets() {
|
|
let refs = |id: &str, src: &str| -> Vec<String> {
|
|
syntax().web_file_references(&lang(id), src).into_iter().map(|r| r.value).collect()
|
|
};
|
|
let html = concat!(
|
|
"<link href=\"css/a.css\"><img srcset=\"a.png 1x, b.png 2x\" src=img/c.png alt=\"x\">\n",
|
|
"<a href=\"#top\" class=\"k\">t</a><div style=\"background: url(d.svg)\"></div>\n",
|
|
"<style>@import \"e.css\"; .x { background: url('f.png') }</style>",
|
|
);
|
|
assert_eq!(refs("html", html), ["css/a.css", "a.png", "b.png", "img/c.png", "#top", "d.svg", "e.css", "f.png"]);
|
|
assert_eq!(refs("css", "@import url(g.css);\n.y { mask: url(\"h.svg#i\") }"), ["g.css", "h.svg#i"]);
|
|
let first = &syntax().web_file_references(&lang("html"), html)[0];
|
|
assert_eq!((first.range.start.col, first.range.end.col), (12, 21));
|
|
}
|
|
|
|
#[test]
|
|
fn literals_are_outermost_comments_and_strings() {
|
|
let src = "// note \"q\"\nfn main() { let s = \"a // b\"; /* c */ let c = 'x'; }\n";
|
|
let found: Vec<(&str, LiteralKind)> =
|
|
syntax().literals(&lang("rust"), src).unwrap().iter().map(|l| (&src[l.start..l.end], l.kind)).collect();
|
|
assert_eq!(
|
|
found,
|
|
vec![
|
|
("// note \"q\"", LiteralKind::Comment),
|
|
("\"a // b\"", LiteralKind::String),
|
|
("/* c */", LiteralKind::Comment),
|
|
("'x'", LiteralKind::String),
|
|
]
|
|
);
|
|
assert!(syntax().literals(&lang("plaintext"), "// x").is_none());
|
|
}
|
|
|
|
/// `name: visibility` for every outline item, depth-first.
|
|
fn visibilities(id: &str, src: &str) -> Vec<(String, Option<Visibility>)> {
|
|
fn go(items: &[OutlineItem], out: &mut Vec<(String, Option<Visibility>)>) {
|
|
for i in items {
|
|
out.push((i.name.clone(), i.visibility));
|
|
go(&i.children, out);
|
|
}
|
|
}
|
|
let mut out = Vec::new();
|
|
go(&syntax().outline(&lang(id), src), &mut out);
|
|
out
|
|
}
|
|
|
|
fn vis_of(list: &[(String, Option<Visibility>)], name: &str) -> Option<Visibility> {
|
|
list.iter().find(|(n, _)| n == name).unwrap_or_else(|| panic!("no {name} in {list:?}")).1
|
|
}
|
|
|
|
#[test]
|
|
fn outline_visibility_rust() {
|
|
let src = "pub struct S { a: i32, pub b: i32, pub(crate) c: i32 }\n\
|
|
trait T { fn req(&self); }\n\
|
|
impl T for S { fn req(&self) {} }\n\
|
|
impl S { pub fn new() {} fn helper() {} }\n\
|
|
fn main() {}\n";
|
|
let v = visibilities("rust", src);
|
|
assert_eq!(vis_of(&v, "S"), Some(Visibility::Public));
|
|
assert_eq!(vis_of(&v, "a"), Some(Visibility::Private));
|
|
assert_eq!(vis_of(&v, "b"), Some(Visibility::Public));
|
|
assert_eq!(vis_of(&v, "c"), Some(Visibility::Package));
|
|
assert_eq!(vis_of(&v, "new"), Some(Visibility::Public));
|
|
assert_eq!(vis_of(&v, "helper"), Some(Visibility::Private));
|
|
assert_eq!(vis_of(&v, "main"), Some(Visibility::Private));
|
|
assert_eq!(vis_of(&v, "impl T for S"), None);
|
|
// Trait members and trait impl members follow the trait.
|
|
assert!(v.iter().filter(|(n, _)| n == "req").all(|(_, vis)| vis.is_none()));
|
|
}
|
|
|
|
#[test]
|
|
fn outline_visibility_java_ts_python() {
|
|
let java = "public class A {\n private int x;\n protected void p() {}\n void pkg() {}\n public A() {}\n}\ninterface I { void m(); }\n";
|
|
let v = visibilities("java", java);
|
|
assert_eq!(vis_of(&v, "x"), Some(Visibility::Private));
|
|
assert_eq!(vis_of(&v, "p"), Some(Visibility::Protected));
|
|
assert_eq!(vis_of(&v, "pkg"), Some(Visibility::Package));
|
|
assert_eq!(vis_of(&v, "A"), Some(Visibility::Public));
|
|
assert_eq!(vis_of(&v, "m"), None);
|
|
|
|
let ts = "class C {\n private a = 1;\n protected b(): void {}\n c(): void {}\n #d = 2;\n}\n";
|
|
let v = visibilities("typescript", ts);
|
|
assert_eq!(vis_of(&v, "a"), Some(Visibility::Private));
|
|
assert_eq!(vis_of(&v, "b"), Some(Visibility::Protected));
|
|
assert_eq!(vis_of(&v, "c"), None);
|
|
assert_eq!(vis_of(&v, "#d"), Some(Visibility::Private));
|
|
|
|
let py = "class K:\n def _hidden(self): pass\n def __init__(self): pass\n def shown(self): pass\n";
|
|
let v = visibilities("python", py);
|
|
assert_eq!(vis_of(&v, "_hidden"), Some(Visibility::Private));
|
|
assert_eq!(vis_of(&v, "__init__"), None);
|
|
assert_eq!(vis_of(&v, "shown"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn merge_visibility_matches_by_name_and_position() {
|
|
fn clear(items: &mut [OutlineItem]) {
|
|
for i in items {
|
|
i.visibility = None;
|
|
clear(&mut i.children);
|
|
}
|
|
}
|
|
let src = "struct S { a: i32 }\nfn f() {}\n";
|
|
let ts = syntax().outline(&lang("rust"), src);
|
|
let mut lsp = ts.clone();
|
|
clear(&mut lsp);
|
|
lsp[1].name = "renamed".into();
|
|
crate::merge_visibility(&mut lsp, &ts);
|
|
assert_eq!(lsp[0].visibility, Some(Visibility::Private));
|
|
assert_eq!(lsp[0].children[0].visibility, Some(Visibility::Private));
|
|
assert_eq!(lsp[1].visibility, None);
|
|
}
|
|
|
|
#[test]
|
|
fn spelling_regions_cover_declared_names_only() {
|
|
let src = "// note\nfn parse_input(raw_text: &str) -> Value { let mut counter = helper(raw_text); \"hi\" }\n";
|
|
let found: Vec<(&str, SpellingKind)> =
|
|
syntax().spelling_regions(&lang("rust"), src).unwrap().iter().map(|r| (&src[r.start..r.end], r.kind)).collect();
|
|
assert_eq!(
|
|
found,
|
|
vec![
|
|
("// note", SpellingKind::Comment),
|
|
("parse_input", SpellingKind::Name),
|
|
("raw_text", SpellingKind::Name),
|
|
("counter", SpellingKind::Name),
|
|
("\"hi\"", SpellingKind::String),
|
|
]
|
|
);
|
|
let py = "def load_config(path, retries=3):\n \"\"\"Doc.\"\"\"\n total = 0\n";
|
|
let names: Vec<&str> = syntax()
|
|
.spelling_regions(&lang("python"), py)
|
|
.unwrap()
|
|
.iter()
|
|
.filter(|r| r.kind == SpellingKind::Name)
|
|
.map(|r| &py[r.start..r.end])
|
|
.collect();
|
|
assert_eq!(names, ["load_config", "path", "retries", "total"]);
|
|
// Names in paths and calls are uses.
|
|
let rs = "use crate::ui::keymap;\nfn run() { let n = std::ffi::OsStr::new(\"x\"); a::b::millis(); }\n";
|
|
let names: Vec<&str> = syntax()
|
|
.spelling_regions(&lang("rust"), rs)
|
|
.unwrap()
|
|
.iter()
|
|
.filter(|r| r.kind == SpellingKind::Name)
|
|
.map(|r| &rs[r.start..r.end])
|
|
.collect();
|
|
assert_eq!(names, ["run", "n"]);
|
|
let java = "class Bird { String fly(String text) { return text.substring(1); } }\n";
|
|
let names: Vec<&str> = syntax()
|
|
.spelling_regions(&lang("java"), java)
|
|
.unwrap()
|
|
.iter()
|
|
.filter(|r| r.kind == SpellingKind::Name)
|
|
.map(|r| &java[r.start..r.end])
|
|
.collect();
|
|
assert_eq!(names, ["Bird", "fly", "text"]);
|
|
}
|