diff options
Diffstat (limited to 'crates/libeditor')
-rw-r--r-- | crates/libeditor/Cargo.toml | 10 | ||||
-rw-r--r-- | crates/libeditor/src/extend_selection.rs | 36 | ||||
-rw-r--r-- | crates/libeditor/src/lib.rs | 155 | ||||
-rw-r--r-- | crates/libeditor/src/line_index.rs | 62 | ||||
-rw-r--r-- | crates/libeditor/tests/test.rs | 69 |
5 files changed, 332 insertions, 0 deletions
diff --git a/crates/libeditor/Cargo.toml b/crates/libeditor/Cargo.toml new file mode 100644 index 000000000..d6423979b --- /dev/null +++ b/crates/libeditor/Cargo.toml | |||
@@ -0,0 +1,10 @@ | |||
1 | [package] | ||
2 | name = "libeditor" | ||
3 | version = "0.1.0" | ||
4 | authors = ["Aleksey Kladov <[email protected]>"] | ||
5 | publish = false | ||
6 | |||
7 | [dependencies] | ||
8 | itertools = "0.7.8" | ||
9 | superslice = "0.1.0" | ||
10 | libsyntax2 = { path = "../libsyntax2" } | ||
diff --git a/crates/libeditor/src/extend_selection.rs b/crates/libeditor/src/extend_selection.rs new file mode 100644 index 000000000..16d4bc084 --- /dev/null +++ b/crates/libeditor/src/extend_selection.rs | |||
@@ -0,0 +1,36 @@ | |||
1 | use libsyntax2::{ | ||
2 | TextRange, SyntaxNodeRef, | ||
3 | SyntaxKind::WHITESPACE, | ||
4 | algo::{find_leaf_at_offset, find_covering_node, ancestors}, | ||
5 | }; | ||
6 | |||
7 | |||
8 | pub(crate) fn extend_selection(root: SyntaxNodeRef, range: TextRange) -> Option<TextRange> { | ||
9 | if range.is_empty() { | ||
10 | let offset = range.start(); | ||
11 | let mut leaves = find_leaf_at_offset(root, offset); | ||
12 | if let Some(leaf) = leaves.clone().find(|node| node.kind() != WHITESPACE) { | ||
13 | return Some(leaf.range()); | ||
14 | } | ||
15 | let ws = leaves.next()?; | ||
16 | // let ws_suffix = file.text().slice( | ||
17 | // TextRange::from_to(offset, ws.range().end()) | ||
18 | // ); | ||
19 | // if ws.text().contains("\n") && !ws_suffix.contains("\n") { | ||
20 | // if let Some(line_end) = file.text() | ||
21 | // .slice(TextSuffix::from(ws.range().end())) | ||
22 | // .find("\n") | ||
23 | // { | ||
24 | // let range = TextRange::from_len(ws.range().end(), line_end); | ||
25 | // return Some(find_covering_node(file.root(), range).range()); | ||
26 | // } | ||
27 | // } | ||
28 | return Some(ws.range()); | ||
29 | }; | ||
30 | let node = find_covering_node(root, range); | ||
31 | |||
32 | match ancestors(node).skip_while(|n| n.range() == range).next() { | ||
33 | None => None, | ||
34 | Some(parent) => Some(parent.range()), | ||
35 | } | ||
36 | } | ||
diff --git a/crates/libeditor/src/lib.rs b/crates/libeditor/src/lib.rs new file mode 100644 index 000000000..f77647338 --- /dev/null +++ b/crates/libeditor/src/lib.rs | |||
@@ -0,0 +1,155 @@ | |||
1 | extern crate libsyntax2; | ||
2 | extern crate superslice; | ||
3 | |||
4 | mod extend_selection; | ||
5 | mod line_index; | ||
6 | |||
7 | use libsyntax2::{ | ||
8 | SyntaxNodeRef, AstNode, | ||
9 | algo::walk, | ||
10 | SyntaxKind::*, | ||
11 | }; | ||
12 | pub use libsyntax2::{TextRange, TextUnit, ast}; | ||
13 | pub use self::line_index::{LineIndex, LineCol}; | ||
14 | |||
15 | #[derive(Debug)] | ||
16 | pub struct HighlightedRange { | ||
17 | pub range: TextRange, | ||
18 | pub tag: &'static str, | ||
19 | } | ||
20 | |||
21 | #[derive(Debug)] | ||
22 | pub struct Diagnostic { | ||
23 | pub range: TextRange, | ||
24 | pub msg: String, | ||
25 | } | ||
26 | |||
27 | #[derive(Debug)] | ||
28 | pub struct Symbol { | ||
29 | // pub parent: ???, | ||
30 | pub name: String, | ||
31 | pub range: TextRange, | ||
32 | } | ||
33 | |||
34 | #[derive(Debug)] | ||
35 | pub struct Runnable { | ||
36 | pub range: TextRange, | ||
37 | pub kind: RunnableKind, | ||
38 | } | ||
39 | |||
40 | #[derive(Debug)] | ||
41 | pub enum RunnableKind { | ||
42 | Test { name: String }, | ||
43 | Bin, | ||
44 | } | ||
45 | |||
46 | pub fn highlight(file: &ast::File) -> Vec<HighlightedRange> { | ||
47 | let syntax = file.syntax(); | ||
48 | let mut res = Vec::new(); | ||
49 | for node in walk::preorder(syntax.as_ref()) { | ||
50 | let tag = match node.kind() { | ||
51 | ERROR => "error", | ||
52 | COMMENT | DOC_COMMENT => "comment", | ||
53 | STRING | RAW_STRING | RAW_BYTE_STRING | BYTE_STRING => "string", | ||
54 | ATTR => "attribute", | ||
55 | NAME_REF => "text", | ||
56 | NAME => "function", | ||
57 | INT_NUMBER | FLOAT_NUMBER | CHAR | BYTE => "literal", | ||
58 | LIFETIME => "parameter", | ||
59 | k if k.is_keyword() => "keyword", | ||
60 | _ => continue, | ||
61 | }; | ||
62 | res.push(HighlightedRange { | ||
63 | range: node.range(), | ||
64 | tag, | ||
65 | }) | ||
66 | } | ||
67 | res | ||
68 | } | ||
69 | |||
70 | pub fn diagnostics(file: &ast::File) -> Vec<Diagnostic> { | ||
71 | let syntax = file.syntax(); | ||
72 | let mut res = Vec::new(); | ||
73 | |||
74 | for node in walk::preorder(syntax.as_ref()) { | ||
75 | if node.kind() == ERROR { | ||
76 | res.push(Diagnostic { | ||
77 | range: node.range(), | ||
78 | msg: "Syntax Error".to_string(), | ||
79 | }); | ||
80 | } | ||
81 | } | ||
82 | res.extend(file.errors().into_iter().map(|err| Diagnostic { | ||
83 | range: TextRange::offset_len(err.offset, 1.into()), | ||
84 | msg: err.msg, | ||
85 | })); | ||
86 | res | ||
87 | } | ||
88 | |||
89 | pub fn syntax_tree(file: &ast::File) -> String { | ||
90 | ::libsyntax2::utils::dump_tree(&file.syntax()) | ||
91 | } | ||
92 | |||
93 | pub fn symbols(file: &ast::File) -> Vec<Symbol> { | ||
94 | let syntax = file.syntax(); | ||
95 | let res: Vec<Symbol> = walk::preorder(syntax.as_ref()) | ||
96 | .filter_map(Declaration::cast) | ||
97 | .filter_map(|decl| { | ||
98 | let name = decl.name()?; | ||
99 | let range = decl.range(); | ||
100 | Some(Symbol { name, range }) | ||
101 | }) | ||
102 | .collect(); | ||
103 | res // NLL :-( | ||
104 | } | ||
105 | |||
106 | pub fn extend_selection(file: &ast::File, range: TextRange) -> Option<TextRange> { | ||
107 | let syntax = file.syntax(); | ||
108 | extend_selection::extend_selection(syntax.as_ref(), range) | ||
109 | } | ||
110 | |||
111 | pub fn runnables(file: &ast::File) -> Vec<Runnable> { | ||
112 | file | ||
113 | .functions() | ||
114 | .filter_map(|f| { | ||
115 | let name = f.name()?.text(); | ||
116 | let kind = if name == "main" { | ||
117 | RunnableKind::Bin | ||
118 | } else if f.has_atom_attr("test") { | ||
119 | RunnableKind::Test { | ||
120 | name: name.to_string() | ||
121 | } | ||
122 | } else { | ||
123 | return None; | ||
124 | }; | ||
125 | Some(Runnable { | ||
126 | range: f.syntax().range(), | ||
127 | kind, | ||
128 | }) | ||
129 | }) | ||
130 | .collect() | ||
131 | } | ||
132 | |||
133 | |||
134 | struct Declaration<'f> (SyntaxNodeRef<'f>); | ||
135 | |||
136 | impl<'f> Declaration<'f> { | ||
137 | fn cast(node: SyntaxNodeRef<'f>) -> Option<Declaration<'f>> { | ||
138 | match node.kind() { | ||
139 | | STRUCT_ITEM | ENUM_ITEM | FUNCTION | TRAIT_ITEM | ||
140 | | CONST_ITEM | STATIC_ITEM | MOD_ITEM | NAMED_FIELD | ||
141 | | TYPE_ITEM => Some(Declaration(node)), | ||
142 | _ => None | ||
143 | } | ||
144 | } | ||
145 | |||
146 | fn name(&self) -> Option<String> { | ||
147 | let name = self.0.children() | ||
148 | .find(|child| child.kind() == NAME)?; | ||
149 | Some(name.text()) | ||
150 | } | ||
151 | |||
152 | fn range(&self) -> TextRange { | ||
153 | self.0.range() | ||
154 | } | ||
155 | } | ||
diff --git a/crates/libeditor/src/line_index.rs b/crates/libeditor/src/line_index.rs new file mode 100644 index 000000000..801726aa5 --- /dev/null +++ b/crates/libeditor/src/line_index.rs | |||
@@ -0,0 +1,62 @@ | |||
1 | use superslice::Ext; | ||
2 | use ::TextUnit; | ||
3 | |||
4 | #[derive(Clone, Debug)] | ||
5 | pub struct LineIndex { | ||
6 | newlines: Vec<TextUnit>, | ||
7 | } | ||
8 | |||
9 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] | ||
10 | pub struct LineCol { | ||
11 | pub line: u32, | ||
12 | pub col: TextUnit, | ||
13 | } | ||
14 | |||
15 | impl LineIndex { | ||
16 | pub fn new(text: &str) -> LineIndex { | ||
17 | let mut newlines = vec![0.into()]; | ||
18 | let mut curr = 0.into(); | ||
19 | for c in text.chars() { | ||
20 | curr += TextUnit::of_char(c); | ||
21 | if c == '\n' { | ||
22 | newlines.push(curr); | ||
23 | } | ||
24 | } | ||
25 | LineIndex { newlines } | ||
26 | } | ||
27 | |||
28 | pub fn line_col(&self, offset: TextUnit) -> LineCol { | ||
29 | let line = self.newlines.upper_bound(&offset) - 1; | ||
30 | let line_start_offset = self.newlines[line]; | ||
31 | let col = offset - line_start_offset; | ||
32 | return LineCol { line: line as u32, col }; | ||
33 | } | ||
34 | |||
35 | pub fn offset(&self, line_col: LineCol) -> TextUnit { | ||
36 | //TODO: return Result | ||
37 | self.newlines[line_col.line as usize] + line_col.col | ||
38 | } | ||
39 | } | ||
40 | |||
41 | #[test] | ||
42 | fn test_line_index() { | ||
43 | let text = "hello\nworld"; | ||
44 | let index = LineIndex::new(text); | ||
45 | assert_eq!(index.line_col(0.into()), LineCol { line: 0, col: 0.into() }); | ||
46 | assert_eq!(index.line_col(1.into()), LineCol { line: 0, col: 1.into() }); | ||
47 | assert_eq!(index.line_col(5.into()), LineCol { line: 0, col: 5.into() }); | ||
48 | assert_eq!(index.line_col(6.into()), LineCol { line: 1, col: 0.into() }); | ||
49 | assert_eq!(index.line_col(7.into()), LineCol { line: 1, col: 1.into() }); | ||
50 | assert_eq!(index.line_col(8.into()), LineCol { line: 1, col: 2.into() }); | ||
51 | assert_eq!(index.line_col(10.into()), LineCol { line: 1, col: 4.into() }); | ||
52 | assert_eq!(index.line_col(11.into()), LineCol { line: 1, col: 5.into() }); | ||
53 | assert_eq!(index.line_col(12.into()), LineCol { line: 1, col: 6.into() }); | ||
54 | |||
55 | let text = "\nhello\nworld"; | ||
56 | let index = LineIndex::new(text); | ||
57 | assert_eq!(index.line_col(0.into()), LineCol { line: 0, col: 0.into() }); | ||
58 | assert_eq!(index.line_col(1.into()), LineCol { line: 1, col: 0.into() }); | ||
59 | assert_eq!(index.line_col(2.into()), LineCol { line: 1, col: 1.into() }); | ||
60 | assert_eq!(index.line_col(6.into()), LineCol { line: 1, col: 5.into() }); | ||
61 | assert_eq!(index.line_col(7.into()), LineCol { line: 2, col: 0.into() }); | ||
62 | } | ||
diff --git a/crates/libeditor/tests/test.rs b/crates/libeditor/tests/test.rs new file mode 100644 index 000000000..2a84c5080 --- /dev/null +++ b/crates/libeditor/tests/test.rs | |||
@@ -0,0 +1,69 @@ | |||
1 | extern crate libeditor; | ||
2 | extern crate itertools; | ||
3 | |||
4 | use std::fmt; | ||
5 | use itertools::Itertools; | ||
6 | use libeditor::{ast, highlight, runnables, extend_selection, TextRange}; | ||
7 | |||
8 | #[test] | ||
9 | fn test_extend_selection() { | ||
10 | let file = file(r#"fn foo() { | ||
11 | 1 + 1 | ||
12 | } | ||
13 | "#); | ||
14 | let range = TextRange::offset_len(18.into(), 0.into()); | ||
15 | let range = extend_selection(&file, range).unwrap(); | ||
16 | assert_eq!(range, TextRange::from_to(17.into(), 18.into())); | ||
17 | let range = extend_selection(&file, range).unwrap(); | ||
18 | assert_eq!(range, TextRange::from_to(15.into(), 20.into())); | ||
19 | } | ||
20 | |||
21 | #[test] | ||
22 | fn test_highlighting() { | ||
23 | let file = file(r#" | ||
24 | // comment | ||
25 | fn main() {} | ||
26 | println!("Hello, {}!", 92); | ||
27 | "#); | ||
28 | let hls = highlight(&file); | ||
29 | dbg_eq( | ||
30 | &hls, | ||
31 | r#"[HighlightedRange { range: [1; 11), tag: "comment" }, | ||
32 | HighlightedRange { range: [12; 14), tag: "keyword" }, | ||
33 | HighlightedRange { range: [15; 19), tag: "function" }, | ||
34 | HighlightedRange { range: [29; 36), tag: "text" }, | ||
35 | HighlightedRange { range: [38; 50), tag: "string" }, | ||
36 | HighlightedRange { range: [52; 54), tag: "literal" }]"# | ||
37 | ); | ||
38 | } | ||
39 | |||
40 | #[test] | ||
41 | fn test_runnables() { | ||
42 | let file = file(r#" | ||
43 | fn main() {} | ||
44 | |||
45 | #[test] | ||
46 | fn test_foo() {} | ||
47 | |||
48 | #[test] | ||
49 | #[ignore] | ||
50 | fn test_foo() {} | ||
51 | "#); | ||
52 | let runnables = runnables(&file); | ||
53 | dbg_eq( | ||
54 | &runnables, | ||
55 | r#"[Runnable { range: [1; 13), kind: Bin }, | ||
56 | Runnable { range: [15; 39), kind: Test { name: "test_foo" } }, | ||
57 | Runnable { range: [41; 75), kind: Test { name: "test_foo" } }]"#, | ||
58 | ) | ||
59 | } | ||
60 | |||
61 | fn file(text: &str) -> ast::File { | ||
62 | ast::File::parse(text) | ||
63 | } | ||
64 | |||
65 | fn dbg_eq(actual: &impl fmt::Debug, expected: &str) { | ||
66 | let actual = format!("{:?}", actual); | ||
67 | let expected = expected.lines().map(|l| l.trim()).join(" "); | ||
68 | assert_eq!(actual, expected); | ||
69 | } | ||