aboutsummaryrefslogtreecommitdiff
path: root/crates/libeditor
diff options
context:
space:
mode:
Diffstat (limited to 'crates/libeditor')
-rw-r--r--crates/libeditor/Cargo.toml10
-rw-r--r--crates/libeditor/src/extend_selection.rs36
-rw-r--r--crates/libeditor/src/lib.rs155
-rw-r--r--crates/libeditor/src/line_index.rs62
-rw-r--r--crates/libeditor/tests/test.rs69
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]
2name = "libeditor"
3version = "0.1.0"
4authors = ["Aleksey Kladov <[email protected]>"]
5publish = false
6
7[dependencies]
8itertools = "0.7.8"
9superslice = "0.1.0"
10libsyntax2 = { 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 @@
1use libsyntax2::{
2 TextRange, SyntaxNodeRef,
3 SyntaxKind::WHITESPACE,
4 algo::{find_leaf_at_offset, find_covering_node, ancestors},
5};
6
7
8pub(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 @@
1extern crate libsyntax2;
2extern crate superslice;
3
4mod extend_selection;
5mod line_index;
6
7use libsyntax2::{
8 SyntaxNodeRef, AstNode,
9 algo::walk,
10 SyntaxKind::*,
11};
12pub use libsyntax2::{TextRange, TextUnit, ast};
13pub use self::line_index::{LineIndex, LineCol};
14
15#[derive(Debug)]
16pub struct HighlightedRange {
17 pub range: TextRange,
18 pub tag: &'static str,
19}
20
21#[derive(Debug)]
22pub struct Diagnostic {
23 pub range: TextRange,
24 pub msg: String,
25}
26
27#[derive(Debug)]
28pub struct Symbol {
29 // pub parent: ???,
30 pub name: String,
31 pub range: TextRange,
32}
33
34#[derive(Debug)]
35pub struct Runnable {
36 pub range: TextRange,
37 pub kind: RunnableKind,
38}
39
40#[derive(Debug)]
41pub enum RunnableKind {
42 Test { name: String },
43 Bin,
44}
45
46pub 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
70pub 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
89pub fn syntax_tree(file: &ast::File) -> String {
90 ::libsyntax2::utils::dump_tree(&file.syntax())
91}
92
93pub 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
106pub 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
111pub 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
134struct Declaration<'f> (SyntaxNodeRef<'f>);
135
136impl<'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 @@
1use superslice::Ext;
2use ::TextUnit;
3
4#[derive(Clone, Debug)]
5pub struct LineIndex {
6 newlines: Vec<TextUnit>,
7}
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
10pub struct LineCol {
11 pub line: u32,
12 pub col: TextUnit,
13}
14
15impl 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]
42fn 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 @@
1extern crate libeditor;
2extern crate itertools;
3
4use std::fmt;
5use itertools::Itertools;
6use libeditor::{ast, highlight, runnables, extend_selection, TextRange};
7
8#[test]
9fn 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]
22fn test_highlighting() {
23 let file = file(r#"
24// comment
25fn 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]
41fn test_runnables() {
42 let file = file(r#"
43fn main() {}
44
45#[test]
46fn test_foo() {}
47
48#[test]
49#[ignore]
50fn 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
61fn file(text: &str) -> ast::File {
62 ast::File::parse(text)
63}
64
65fn 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}