T17 Editor: CodeEditor with gutter, folds, minimap, rulers, guides, brackets, whitespace, cursors, multi-cursor, sticky scroll, inlay hints, lenses, decorations, ghost text, inline chat and diff (T17a); completions, signatures, hover, code actions, rename, references, outline, go to symbol, call and type hierarchy (T17b); find and replace, search panel and filters (T17c); problems, output, debug console, extensions, settings with JSON, keybindings, theme previews, welcome, recent projects, switcher, task runner (T17d); status items (T17e)
Shared: generic undo History, OverflowMenu icon and tooltip, StatusBarItem leading, six Lucide icons.
This commit is contained in:
1 parent
a894e5f973
commit
5545ce6458
59 files changed
+10820
-71
No files matched your search
@@ -0,0 +1,183 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use gpui::{App, HighlightStyle};
|
||||
|
||||
use super::buffer::Buffer;
|
||||
use crate::forms::{Kind, code_highlights, lex};
|
||||
|
||||
/// Each line that opens a block, and the last line the block holds, read from indentation.
|
||||
pub(crate) fn folds(buffer: &Buffer) -> Vec<(usize, usize)> {
|
||||
let blank = |line: usize| buffer.line(line).trim().is_empty();
|
||||
let mut out = Vec::new();
|
||||
for line in 0..buffer.lines() {
|
||||
if blank(line) {
|
||||
continue;
|
||||
}
|
||||
let indent = buffer.indent(line);
|
||||
let mut end = None;
|
||||
for next in line + 1..buffer.lines() {
|
||||
if blank(next) {
|
||||
continue;
|
||||
}
|
||||
if buffer.indent(next) <= indent {
|
||||
break;
|
||||
}
|
||||
end = Some(next);
|
||||
}
|
||||
if let Some(end) = end {
|
||||
out.push((line, end));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A bracket in the code, how deep it sits, and where its partner is when it has one.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct Bracket {
|
||||
pub offset: usize,
|
||||
pub depth: usize,
|
||||
pub partner: Option<usize>,
|
||||
}
|
||||
|
||||
fn opener(close: char) -> char {
|
||||
match close {
|
||||
')' => '(',
|
||||
']' => '[',
|
||||
_ => '{',
|
||||
}
|
||||
}
|
||||
|
||||
/// Every bracket outside strings and comments; a closer takes its opener's depth.
|
||||
pub(crate) fn brackets(buffer: &Buffer) -> Vec<Bracket> {
|
||||
let mut out: Vec<Bracket> = Vec::new();
|
||||
let mut open: Vec<(usize, char)> = Vec::new();
|
||||
for line in 0..buffer.lines() {
|
||||
let start = buffer.line_range(line).start;
|
||||
let text = buffer.line(line);
|
||||
for (range, kind) in lex(text) {
|
||||
if kind != Kind::Mark {
|
||||
continue;
|
||||
}
|
||||
let Some(ch) = text[range.clone()].chars().next() else {
|
||||
continue;
|
||||
};
|
||||
let offset = start + range.start;
|
||||
match ch {
|
||||
'(' | '[' | '{' => {
|
||||
open.push((out.len(), ch));
|
||||
out.push(Bracket {
|
||||
offset,
|
||||
depth: open.len() - 1,
|
||||
partner: None,
|
||||
});
|
||||
}
|
||||
')' | ']' | '}' => match open.last() {
|
||||
Some((ix, wanted)) if *wanted == opener(ch) => {
|
||||
let ix = *ix;
|
||||
open.pop();
|
||||
let partner = out[ix].offset;
|
||||
out[ix].partner = Some(offset);
|
||||
out.push(Bracket {
|
||||
offset,
|
||||
depth: open.len(),
|
||||
partner: Some(partner),
|
||||
});
|
||||
}
|
||||
_ => out.push(Bracket {
|
||||
offset,
|
||||
depth: open.len(),
|
||||
partner: None,
|
||||
}),
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The pair beside a caret: the bracket just after it, or else the one just before.
|
||||
pub(crate) fn matched(brackets: &[Bracket], caret: usize) -> Option<(usize, usize)> {
|
||||
let at = |offset: usize| {
|
||||
brackets
|
||||
.binary_search_by_key(&offset, |bracket| bracket.offset)
|
||||
.ok()
|
||||
.map(|ix| brackets[ix])
|
||||
};
|
||||
let bracket = at(caret).or_else(|| caret.checked_sub(1).and_then(at))?;
|
||||
Some((bracket.offset, bracket.partner?))
|
||||
}
|
||||
|
||||
/// A line's syntax colors as highlight styles.
|
||||
pub(crate) fn colors(text: &str, cx: &App) -> Vec<(Range<usize>, HighlightStyle)> {
|
||||
code_highlights(text, cx)
|
||||
.into_iter()
|
||||
.map(|(range, highlight)| {
|
||||
(
|
||||
range,
|
||||
HighlightStyle {
|
||||
color: Some(highlight.color),
|
||||
background_color: highlight.background,
|
||||
..HighlightStyle::default()
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Where `needle` appears in the lines of `range`, as offsets, whole words only when it is a word.
|
||||
pub(crate) fn occurrences(buffer: &Buffer, needle: &str, lines: Range<usize>) -> Vec<Range<usize>> {
|
||||
if needle.is_empty() || needle.contains('\n') {
|
||||
return Vec::new();
|
||||
}
|
||||
let word = needle.chars().all(|ch| ch.is_alphanumeric() || ch == '_');
|
||||
let boundary = |ch: Option<char>| ch.is_none_or(|ch| !(ch.is_alphanumeric() || ch == '_'));
|
||||
let mut out = Vec::new();
|
||||
for line in lines.start..lines.end.min(buffer.lines()) {
|
||||
let start = buffer.line_range(line).start;
|
||||
let text = buffer.line(line);
|
||||
for (at, _) in text.match_indices(needle) {
|
||||
let end = at + needle.len();
|
||||
let whole =
|
||||
boundary(text[..at].chars().next_back()) && boundary(text[end..].chars().next());
|
||||
if !word || whole {
|
||||
out.push(start + at..start + end);
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn blocks_fold_by_indentation_and_skip_blank_lines() {
|
||||
let buffer = Buffer::new("fn a() {\n one();\n\n two();\n}\nfn b() {}");
|
||||
assert_eq!(folds(&buffer), [(0, 3)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn brackets_pair_by_depth_outside_strings_and_comments() {
|
||||
let buffer = Buffer::new("f(a[0], \"(\") // )\n{ }");
|
||||
let found = brackets(&buffer);
|
||||
let offsets: Vec<usize> = found.iter().map(|bracket| bracket.offset).collect();
|
||||
assert_eq!(
|
||||
offsets,
|
||||
[1, 3, 5, 11, 18, 20],
|
||||
"the quoted and the commented ones do not count"
|
||||
);
|
||||
assert_eq!(found[1].depth, 1);
|
||||
assert_eq!(found[3].partner, Some(1));
|
||||
assert_eq!(matched(&found, 12), Some((11, 1)), "just before the caret");
|
||||
assert_eq!(matched(&found, 18), Some((18, 20)), "just after the caret");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn occurrences_of_a_word_skip_longer_words() {
|
||||
let buffer = Buffer::new("let total = total_cost + total;");
|
||||
let found = occurrences(&buffer, "total", 0..1);
|
||||
assert_eq!(found, [4..9, 25..30]);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user