mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-04 06:34:26 +00:00
Merge branch 'main' into test-branch
This commit is contained in:
commit
41590ef64b
92 changed files with 4166 additions and 2520 deletions
22
.github/workflows/discord_webhook.yml
vendored
22
.github/workflows/discord_webhook.yml
vendored
|
@ -1,22 +0,0 @@
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
message:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Discord Webhook Action
|
|
||||||
uses: tsickert/discord-webhook@v5.3.0
|
|
||||||
with:
|
|
||||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
|
||||||
content: |
|
|
||||||
📣 Zed ${{ github.event.release.name }} was just released!
|
|
||||||
|
|
||||||
Restart your Zed or head to https://zed.dev/releases to grab it.
|
|
||||||
|
|
||||||
```md
|
|
||||||
### Changelog
|
|
||||||
|
|
||||||
${{ github.event.release.body }}
|
|
||||||
```
|
|
33
.github/workflows/release_actions.yml
vendored
Normal file
33
.github/workflows/release_actions.yml
vendored
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
discord_release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Discord Webhook Action
|
||||||
|
uses: tsickert/discord-webhook@v5.3.0
|
||||||
|
with:
|
||||||
|
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||||
|
content: |
|
||||||
|
📣 Zed ${{ github.event.release.tag_name }} was just released!
|
||||||
|
|
||||||
|
Restart your Zed or head to https://zed.dev/releases to grab it.
|
||||||
|
|
||||||
|
```md
|
||||||
|
### Changelog
|
||||||
|
|
||||||
|
${{ github.event.release.body }}
|
||||||
|
```
|
||||||
|
amplitude_release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: "3.10.5"
|
||||||
|
architecture: "x64"
|
||||||
|
cache: "pip"
|
||||||
|
- run: pip install -r script/amplitude_release/requirements.txt
|
||||||
|
- run: python script/amplitude_release/main.py ${{ github.event.release.tag_name }} ${{ secrets.ZED_AMPLITUDE_API_KEY }} ${{ secrets.ZED_AMPLITUDE_SECRET_KEY }}
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -8,4 +8,5 @@
|
||||||
/vendor/bin
|
/vendor/bin
|
||||||
/assets/themes/*.json
|
/assets/themes/*.json
|
||||||
/assets/themes/internal/*.json
|
/assets/themes/internal/*.json
|
||||||
/assets/themes/experiments/*.json
|
/assets/themes/experiments/*.json
|
||||||
|
**/venv
|
|
@ -9,11 +9,10 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"h": "vim::Left",
|
"h": "vim::Left",
|
||||||
"backspace": "vim::Left",
|
"backspace": "vim::Backspace",
|
||||||
"j": "vim::Down",
|
"j": "vim::Down",
|
||||||
"k": "vim::Up",
|
"k": "vim::Up",
|
||||||
"l": "vim::Right",
|
"l": "vim::Right",
|
||||||
"0": "vim::StartOfLine",
|
|
||||||
"$": "vim::EndOfLine",
|
"$": "vim::EndOfLine",
|
||||||
"shift-g": "vim::EndOfDocument",
|
"shift-g": "vim::EndOfDocument",
|
||||||
"w": "vim::NextWordStart",
|
"w": "vim::NextWordStart",
|
||||||
|
@ -38,7 +37,60 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"%": "vim::Matching",
|
"%": "vim::Matching",
|
||||||
"escape": "editor::Cancel"
|
"escape": "editor::Cancel",
|
||||||
|
"i": [
|
||||||
|
"vim::PushOperator",
|
||||||
|
{
|
||||||
|
"Object": {
|
||||||
|
"around": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"a": [
|
||||||
|
"vim::PushOperator",
|
||||||
|
{
|
||||||
|
"Object": {
|
||||||
|
"around": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
|
||||||
|
"1": [
|
||||||
|
"vim::Number",
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"2": [
|
||||||
|
"vim::Number",
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"3": [
|
||||||
|
"vim::Number",
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"4": [
|
||||||
|
"vim::Number",
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"5": [
|
||||||
|
"vim::Number",
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"6": [
|
||||||
|
"vim::Number",
|
||||||
|
6
|
||||||
|
],
|
||||||
|
"7": [
|
||||||
|
"vim::Number",
|
||||||
|
7
|
||||||
|
],
|
||||||
|
"8": [
|
||||||
|
"vim::Number",
|
||||||
|
8
|
||||||
|
],
|
||||||
|
"9": [
|
||||||
|
"vim::Number",
|
||||||
|
9
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -98,6 +150,15 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "Editor && vim_operator == n",
|
||||||
|
"bindings": {
|
||||||
|
"0": [
|
||||||
|
"vim::Number",
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && vim_operator == g",
|
"context": "Editor && vim_operator == g",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
@ -112,13 +173,6 @@
|
||||||
{
|
{
|
||||||
"context": "Editor && vim_operator == c",
|
"context": "Editor && vim_operator == c",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"w": "vim::ChangeWord",
|
|
||||||
"shift-w": [
|
|
||||||
"vim::ChangeWord",
|
|
||||||
{
|
|
||||||
"ignorePunctuation": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"c": "vim::CurrentLine"
|
"c": "vim::CurrentLine"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -134,9 +188,34 @@
|
||||||
"y": "vim::CurrentLine"
|
"y": "vim::CurrentLine"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "Editor && VimObject",
|
||||||
|
"bindings": {
|
||||||
|
"w": "vim::Word",
|
||||||
|
"shift-w": [
|
||||||
|
"vim::Word",
|
||||||
|
{
|
||||||
|
"ignorePunctuation": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"s": "vim::Sentence",
|
||||||
|
"'": "vim::Quotes",
|
||||||
|
"`": "vim::BackQuotes",
|
||||||
|
"\"": "vim::DoubleQuotes",
|
||||||
|
"(": "vim::Parentheses",
|
||||||
|
")": "vim::Parentheses",
|
||||||
|
"[": "vim::SquareBrackets",
|
||||||
|
"]": "vim::SquareBrackets",
|
||||||
|
"{": "vim::CurlyBrackets",
|
||||||
|
"}": "vim::CurlyBrackets",
|
||||||
|
"<": "vim::AngleBrackets",
|
||||||
|
">": "vim::AngleBrackets"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && vim_mode == visual",
|
"context": "Editor && vim_mode == visual",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
"u": "editor::Undo",
|
||||||
"c": "vim::VisualChange",
|
"c": "vim::VisualChange",
|
||||||
"d": "vim::VisualDelete",
|
"d": "vim::VisualDelete",
|
||||||
"x": "vim::VisualDelete",
|
"x": "vim::VisualDelete",
|
||||||
|
|
|
@ -398,11 +398,11 @@ impl Room {
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
let response = request.await?;
|
let response = request.await?;
|
||||||
|
|
||||||
project
|
project.update(&mut cx, |project, cx| {
|
||||||
.update(&mut cx, |project, cx| {
|
project
|
||||||
project.shared(response.project_id, cx)
|
.shared(response.project_id, cx)
|
||||||
})
|
.detach_and_log_err(cx)
|
||||||
.await?;
|
});
|
||||||
|
|
||||||
// If the user's location is in this project, it changes from UnsharedProject to SharedProject.
|
// If the user's location is in this project, it changes from UnsharedProject to SharedProject.
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
|
|
|
@ -3874,6 +3874,7 @@ async fn test_language_server_statuses(
|
||||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
deterministic.run_until_parked();
|
||||||
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||||
project_b.read_with(cx_b, |project, _| {
|
project_b.read_with(cx_b, |project, _| {
|
||||||
let status = project.language_server_statuses().next().unwrap();
|
let status = project.language_server_statuses().next().unwrap();
|
||||||
|
@ -5522,6 +5523,7 @@ async fn test_random_collaboration(
|
||||||
cx.font_cache(),
|
cx.font_cache(),
|
||||||
cx.leak_detector(),
|
cx.leak_detector(),
|
||||||
next_entity_id,
|
next_entity_id,
|
||||||
|
cx.function_name.clone(),
|
||||||
);
|
);
|
||||||
let host = server.create_client(&mut host_cx, "host").await;
|
let host = server.create_client(&mut host_cx, "host").await;
|
||||||
let host_project = host_cx.update(|cx| {
|
let host_project = host_cx.update(|cx| {
|
||||||
|
@ -5763,6 +5765,7 @@ async fn test_random_collaboration(
|
||||||
cx.font_cache(),
|
cx.font_cache(),
|
||||||
cx.leak_detector(),
|
cx.leak_detector(),
|
||||||
next_entity_id,
|
next_entity_id,
|
||||||
|
cx.function_name.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
deterministic.start_waiting();
|
deterministic.start_waiting();
|
||||||
|
|
|
@ -65,7 +65,6 @@ enum ContactEntry {
|
||||||
project_id: u64,
|
project_id: u64,
|
||||||
worktree_root_names: Vec<String>,
|
worktree_root_names: Vec<String>,
|
||||||
host_user_id: u64,
|
host_user_id: u64,
|
||||||
is_host: bool,
|
|
||||||
is_last: bool,
|
is_last: bool,
|
||||||
},
|
},
|
||||||
IncomingRequest(Arc<User>),
|
IncomingRequest(Arc<User>),
|
||||||
|
@ -181,6 +180,7 @@ impl ContactList {
|
||||||
let list_state = ListState::new(0, Orientation::Top, 1000., cx, move |this, ix, cx| {
|
let list_state = ListState::new(0, Orientation::Top, 1000., cx, move |this, ix, cx| {
|
||||||
let theme = cx.global::<Settings>().theme.clone();
|
let theme = cx.global::<Settings>().theme.clone();
|
||||||
let is_selected = this.selection == Some(ix);
|
let is_selected = this.selection == Some(ix);
|
||||||
|
let current_project_id = this.project.read(cx).remote_id();
|
||||||
|
|
||||||
match &this.entries[ix] {
|
match &this.entries[ix] {
|
||||||
ContactEntry::Header(section) => {
|
ContactEntry::Header(section) => {
|
||||||
|
@ -205,13 +205,12 @@ impl ContactList {
|
||||||
project_id,
|
project_id,
|
||||||
worktree_root_names,
|
worktree_root_names,
|
||||||
host_user_id,
|
host_user_id,
|
||||||
is_host,
|
|
||||||
is_last,
|
is_last,
|
||||||
} => Self::render_participant_project(
|
} => Self::render_participant_project(
|
||||||
*project_id,
|
*project_id,
|
||||||
worktree_root_names,
|
worktree_root_names,
|
||||||
*host_user_id,
|
*host_user_id,
|
||||||
*is_host,
|
Some(*project_id) == current_project_id,
|
||||||
*is_last,
|
*is_last,
|
||||||
is_selected,
|
is_selected,
|
||||||
&theme.contact_list,
|
&theme.contact_list,
|
||||||
|
@ -341,15 +340,12 @@ impl ContactList {
|
||||||
ContactEntry::ParticipantProject {
|
ContactEntry::ParticipantProject {
|
||||||
project_id,
|
project_id,
|
||||||
host_user_id,
|
host_user_id,
|
||||||
is_host,
|
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
if !is_host {
|
cx.dispatch_global_action(JoinProject {
|
||||||
cx.dispatch_global_action(JoinProject {
|
project_id: *project_id,
|
||||||
project_id: *project_id,
|
follow_user_id: *host_user_id,
|
||||||
follow_user_id: *host_user_id,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
@ -407,7 +403,6 @@ impl ContactList {
|
||||||
project_id: project.id,
|
project_id: project.id,
|
||||||
worktree_root_names: project.worktree_root_names.clone(),
|
worktree_root_names: project.worktree_root_names.clone(),
|
||||||
host_user_id: user_id,
|
host_user_id: user_id,
|
||||||
is_host: true,
|
|
||||||
is_last: projects.peek().is_none(),
|
is_last: projects.peek().is_none(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -448,7 +443,6 @@ impl ContactList {
|
||||||
project_id: project.id,
|
project_id: project.id,
|
||||||
worktree_root_names: project.worktree_root_names.clone(),
|
worktree_root_names: project.worktree_root_names.clone(),
|
||||||
host_user_id: participant.user.id,
|
host_user_id: participant.user.id,
|
||||||
is_host: false,
|
|
||||||
is_last: projects.peek().is_none(),
|
is_last: projects.peek().is_none(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -667,7 +661,7 @@ impl ContactList {
|
||||||
project_id: u64,
|
project_id: u64,
|
||||||
worktree_root_names: &[String],
|
worktree_root_names: &[String],
|
||||||
host_user_id: u64,
|
host_user_id: u64,
|
||||||
is_host: bool,
|
is_current: bool,
|
||||||
is_last: bool,
|
is_last: bool,
|
||||||
is_selected: bool,
|
is_selected: bool,
|
||||||
theme: &theme::ContactList,
|
theme: &theme::ContactList,
|
||||||
|
@ -749,13 +743,13 @@ impl ContactList {
|
||||||
.with_style(row.container)
|
.with_style(row.container)
|
||||||
.boxed()
|
.boxed()
|
||||||
})
|
})
|
||||||
.with_cursor_style(if !is_host {
|
.with_cursor_style(if !is_current {
|
||||||
CursorStyle::PointingHand
|
CursorStyle::PointingHand
|
||||||
} else {
|
} else {
|
||||||
CursorStyle::Arrow
|
CursorStyle::Arrow
|
||||||
})
|
})
|
||||||
.on_click(MouseButton::Left, move |_, cx| {
|
.on_click(MouseButton::Left, move |_, cx| {
|
||||||
if !is_host {
|
if !is_current {
|
||||||
cx.dispatch_global_action(JoinProject {
|
cx.dispatch_global_action(JoinProject {
|
||||||
project_id,
|
project_id,
|
||||||
follow_user_id: host_user_id,
|
follow_user_id: host_user_id,
|
||||||
|
|
|
@ -331,34 +331,91 @@ impl DisplaySnapshot {
|
||||||
DisplayPoint(self.blocks_snapshot.max_point())
|
DisplayPoint(self.blocks_snapshot.max_point())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns text chunks starting at the given display row until the end of the file
|
||||||
pub fn text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
|
pub fn text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
|
||||||
self.blocks_snapshot
|
self.blocks_snapshot
|
||||||
.chunks(display_row..self.max_point().row() + 1, false, None)
|
.chunks(display_row..self.max_point().row() + 1, false, None)
|
||||||
.map(|h| h.text)
|
.map(|h| h.text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns text chunks starting at the end of the given display row in reverse until the start of the file
|
||||||
|
pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
|
||||||
|
(0..=display_row).into_iter().rev().flat_map(|row| {
|
||||||
|
self.blocks_snapshot
|
||||||
|
.chunks(row..row + 1, false, None)
|
||||||
|
.map(|h| h.text)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.into_iter()
|
||||||
|
.rev()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn chunks(&self, display_rows: Range<u32>, language_aware: bool) -> DisplayChunks<'_> {
|
pub fn chunks(&self, display_rows: Range<u32>, language_aware: bool) -> DisplayChunks<'_> {
|
||||||
self.blocks_snapshot
|
self.blocks_snapshot
|
||||||
.chunks(display_rows, language_aware, Some(&self.text_highlights))
|
.chunks(display_rows, language_aware, Some(&self.text_highlights))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn chars_at(&self, point: DisplayPoint) -> impl Iterator<Item = char> + '_ {
|
pub fn chars_at(
|
||||||
let mut column = 0;
|
&self,
|
||||||
let mut chars = self.text_chunks(point.row()).flat_map(str::chars);
|
mut point: DisplayPoint,
|
||||||
while column < point.column() {
|
) -> impl Iterator<Item = (char, DisplayPoint)> + '_ {
|
||||||
if let Some(c) = chars.next() {
|
point = DisplayPoint(self.blocks_snapshot.clip_point(point.0, Bias::Left));
|
||||||
column += c.len_utf8() as u32;
|
self.text_chunks(point.row())
|
||||||
} else {
|
.flat_map(str::chars)
|
||||||
break;
|
.skip_while({
|
||||||
}
|
let mut column = 0;
|
||||||
}
|
move |char| {
|
||||||
chars
|
let at_point = column >= point.column();
|
||||||
|
column += char.len_utf8() as u32;
|
||||||
|
!at_point
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(move |ch| {
|
||||||
|
let result = (ch, point);
|
||||||
|
if ch == '\n' {
|
||||||
|
*point.row_mut() += 1;
|
||||||
|
*point.column_mut() = 0;
|
||||||
|
} else {
|
||||||
|
*point.column_mut() += ch.len_utf8() as u32;
|
||||||
|
}
|
||||||
|
result
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reverse_chars_at(
|
||||||
|
&self,
|
||||||
|
mut point: DisplayPoint,
|
||||||
|
) -> impl Iterator<Item = (char, DisplayPoint)> + '_ {
|
||||||
|
point = DisplayPoint(self.blocks_snapshot.clip_point(point.0, Bias::Left));
|
||||||
|
self.reverse_text_chunks(point.row())
|
||||||
|
.flat_map(|chunk| chunk.chars().rev())
|
||||||
|
.skip_while({
|
||||||
|
let mut column = self.line_len(point.row());
|
||||||
|
if self.max_point().row() > point.row() {
|
||||||
|
column += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
move |char| {
|
||||||
|
let at_point = column <= point.column();
|
||||||
|
column = column.saturating_sub(char.len_utf8() as u32);
|
||||||
|
!at_point
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(move |ch| {
|
||||||
|
if ch == '\n' {
|
||||||
|
*point.row_mut() -= 1;
|
||||||
|
*point.column_mut() = self.line_len(point.row());
|
||||||
|
} else {
|
||||||
|
*point.column_mut() = point.column().saturating_sub(ch.len_utf8() as u32);
|
||||||
|
}
|
||||||
|
(ch, point)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn column_to_chars(&self, display_row: u32, target: u32) -> u32 {
|
pub fn column_to_chars(&self, display_row: u32, target: u32) -> u32 {
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
let mut column = 0;
|
let mut column = 0;
|
||||||
for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
|
for (c, _) in self.chars_at(DisplayPoint::new(display_row, 0)) {
|
||||||
if column >= target {
|
if column >= target {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -371,7 +428,7 @@ impl DisplaySnapshot {
|
||||||
pub fn column_from_chars(&self, display_row: u32, char_count: u32) -> u32 {
|
pub fn column_from_chars(&self, display_row: u32, char_count: u32) -> u32 {
|
||||||
let mut column = 0;
|
let mut column = 0;
|
||||||
|
|
||||||
for (count, c) in self.chars_at(DisplayPoint::new(display_row, 0)).enumerate() {
|
for (count, (c, _)) in self.chars_at(DisplayPoint::new(display_row, 0)).enumerate() {
|
||||||
if c == '\n' || count >= char_count as usize {
|
if c == '\n' || count >= char_count as usize {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -455,7 +512,7 @@ impl DisplaySnapshot {
|
||||||
pub fn line_indent(&self, display_row: u32) -> (u32, bool) {
|
pub fn line_indent(&self, display_row: u32) -> (u32, bool) {
|
||||||
let mut indent = 0;
|
let mut indent = 0;
|
||||||
let mut is_blank = true;
|
let mut is_blank = true;
|
||||||
for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
|
for (c, _) in self.chars_at(DisplayPoint::new(display_row, 0)) {
|
||||||
if c == ' ' {
|
if c == ' ' {
|
||||||
indent += 1;
|
indent += 1;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -77,6 +77,7 @@ use util::{post_inc, ResultExt, TryFutureExt};
|
||||||
use workspace::{ItemNavHistory, Workspace};
|
use workspace::{ItemNavHistory, Workspace};
|
||||||
|
|
||||||
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
|
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
|
||||||
|
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
|
||||||
const MAX_LINE_LEN: usize = 1024;
|
const MAX_LINE_LEN: usize = 1024;
|
||||||
const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
|
const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
|
||||||
const MAX_SELECTION_HISTORY_LEN: usize = 1024;
|
const MAX_SELECTION_HISTORY_LEN: usize = 1024;
|
||||||
|
@ -239,6 +240,9 @@ pub enum Direction {
|
||||||
Next,
|
Next,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct ScrollbarAutoHide(bool);
|
||||||
|
|
||||||
pub fn init(cx: &mut MutableAppContext) {
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
cx.add_action(Editor::new_file);
|
cx.add_action(Editor::new_file);
|
||||||
cx.add_action(|this: &mut Editor, action: &Scroll, cx| this.set_scroll_position(action.0, cx));
|
cx.add_action(|this: &mut Editor, action: &Scroll, cx| this.set_scroll_position(action.0, cx));
|
||||||
|
@ -428,6 +432,8 @@ pub struct Editor {
|
||||||
focused: bool,
|
focused: bool,
|
||||||
show_local_cursors: bool,
|
show_local_cursors: bool,
|
||||||
show_local_selections: bool,
|
show_local_selections: bool,
|
||||||
|
show_scrollbars: bool,
|
||||||
|
hide_scrollbar_task: Option<Task<()>>,
|
||||||
blink_epoch: usize,
|
blink_epoch: usize,
|
||||||
blinking_paused: bool,
|
blinking_paused: bool,
|
||||||
mode: EditorMode,
|
mode: EditorMode,
|
||||||
|
@ -1030,6 +1036,8 @@ impl Editor {
|
||||||
focused: false,
|
focused: false,
|
||||||
show_local_cursors: false,
|
show_local_cursors: false,
|
||||||
show_local_selections: true,
|
show_local_selections: true,
|
||||||
|
show_scrollbars: true,
|
||||||
|
hide_scrollbar_task: None,
|
||||||
blink_epoch: 0,
|
blink_epoch: 0,
|
||||||
blinking_paused: false,
|
blinking_paused: false,
|
||||||
mode,
|
mode,
|
||||||
|
@ -1062,10 +1070,16 @@ impl Editor {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
this.end_selection(cx);
|
this.end_selection(cx);
|
||||||
|
this.make_scrollbar_visible(cx);
|
||||||
|
|
||||||
let editor_created_event = EditorCreated(cx.handle());
|
let editor_created_event = EditorCreated(cx.handle());
|
||||||
cx.emit_global(editor_created_event);
|
cx.emit_global(editor_created_event);
|
||||||
|
|
||||||
|
if mode == EditorMode::Full {
|
||||||
|
let should_auto_hide_scrollbars = cx.platform().should_auto_hide_scrollbars();
|
||||||
|
cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars));
|
||||||
|
}
|
||||||
|
|
||||||
this.report_event("open editor", cx);
|
this.report_event("open editor", cx);
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
@ -1182,6 +1196,7 @@ impl Editor {
|
||||||
self.scroll_top_anchor = anchor;
|
self.scroll_top_anchor = anchor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.make_scrollbar_visible(cx);
|
||||||
self.autoscroll_request.take();
|
self.autoscroll_request.take();
|
||||||
hide_hover(self, cx);
|
hide_hover(self, cx);
|
||||||
|
|
||||||
|
@ -1257,7 +1272,7 @@ impl Editor {
|
||||||
let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
|
let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
|
||||||
(display_map.max_point().row() as f32 - visible_lines + 1.).max(0.)
|
(display_map.max_point().row() as f32 - visible_lines + 1.).max(0.)
|
||||||
} else {
|
} else {
|
||||||
display_map.max_point().row().saturating_sub(1) as f32
|
display_map.max_point().row() as f32
|
||||||
};
|
};
|
||||||
if scroll_position.y() > max_scroll_top {
|
if scroll_position.y() > max_scroll_top {
|
||||||
scroll_position.set_y(max_scroll_top);
|
scroll_position.set_y(max_scroll_top);
|
||||||
|
@ -4081,7 +4096,7 @@ impl Editor {
|
||||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||||
s.move_cursors_with(|map, head, _| {
|
s.move_cursors_with(|map, head, _| {
|
||||||
(
|
(
|
||||||
movement::line_beginning(map, head, true),
|
movement::indented_line_beginning(map, head, true),
|
||||||
SelectionGoal::None,
|
SelectionGoal::None,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
@ -4096,7 +4111,7 @@ impl Editor {
|
||||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||||
s.move_heads_with(|map, head, _| {
|
s.move_heads_with(|map, head, _| {
|
||||||
(
|
(
|
||||||
movement::line_beginning(map, head, action.stop_at_soft_wraps),
|
movement::indented_line_beginning(map, head, action.stop_at_soft_wraps),
|
||||||
SelectionGoal::None,
|
SelectionGoal::None,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
@ -5953,6 +5968,31 @@ impl Editor {
|
||||||
self.show_local_cursors && self.focused
|
self.show_local_cursors && self.focused
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn show_scrollbars(&self) -> bool {
|
||||||
|
self.show_scrollbars
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_scrollbar_visible(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
if !self.show_scrollbars {
|
||||||
|
self.show_scrollbars = true;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
if cx.default_global::<ScrollbarAutoHide>().0 {
|
||||||
|
self.hide_scrollbar_task = Some(cx.spawn_weak(|this, mut cx| async move {
|
||||||
|
Timer::after(SCROLLBAR_SHOW_INTERVAL).await;
|
||||||
|
if let Some(this) = this.upgrade(&cx) {
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.show_scrollbars = false;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
self.hide_scrollbar_task = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn on_buffer_changed(&mut self, _: ModelHandle<MultiBuffer>, cx: &mut ViewContext<Self>) {
|
fn on_buffer_changed(&mut self, _: ModelHandle<MultiBuffer>, cx: &mut ViewContext<Self>) {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,22 @@
|
||||||
|
use std::{cell::RefCell, rc::Rc, time::Instant};
|
||||||
|
|
||||||
|
use futures::StreamExt;
|
||||||
|
use indoc::indoc;
|
||||||
|
use unindent::Unindent;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::test::{
|
use crate::test::{
|
||||||
assert_text_with_selections, build_editor, select_ranges, EditorLspTestContext,
|
assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext,
|
||||||
EditorTestContext,
|
editor_test_context::EditorTestContext, select_ranges,
|
||||||
};
|
};
|
||||||
use futures::StreamExt;
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
geometry::rect::RectF,
|
geometry::rect::RectF,
|
||||||
platform::{WindowBounds, WindowOptions},
|
platform::{WindowBounds, WindowOptions},
|
||||||
};
|
};
|
||||||
use indoc::indoc;
|
|
||||||
use language::{FakeLspAdapter, LanguageConfig, LanguageRegistry};
|
use language::{FakeLspAdapter, LanguageConfig, LanguageRegistry};
|
||||||
use project::FakeFs;
|
use project::FakeFs;
|
||||||
use rope::point::Point;
|
use rope::point::Point;
|
||||||
use settings::EditorSettings;
|
use settings::EditorSettings;
|
||||||
use std::{cell::RefCell, rc::Rc, time::Instant};
|
|
||||||
use unindent::Unindent;
|
|
||||||
use util::{
|
use util::{
|
||||||
assert_set_eq,
|
assert_set_eq,
|
||||||
test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker},
|
test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker},
|
||||||
|
|
|
@ -44,7 +44,7 @@ use std::{
|
||||||
cmp::{self, Ordering},
|
cmp::{self, Ordering},
|
||||||
fmt::Write,
|
fmt::Write,
|
||||||
iter,
|
iter,
|
||||||
ops::Range,
|
ops::{DerefMut, Range},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
use theme::DiffStyle;
|
use theme::DiffStyle;
|
||||||
|
@ -455,7 +455,6 @@ impl EditorElement {
|
||||||
let bounds = gutter_bounds.union_rect(text_bounds);
|
let bounds = gutter_bounds.union_rect(text_bounds);
|
||||||
let scroll_top =
|
let scroll_top =
|
||||||
layout.position_map.snapshot.scroll_position().y() * layout.position_map.line_height;
|
layout.position_map.snapshot.scroll_position().y() * layout.position_map.line_height;
|
||||||
let editor = self.view(cx.app);
|
|
||||||
cx.scene.push_quad(Quad {
|
cx.scene.push_quad(Quad {
|
||||||
bounds: gutter_bounds,
|
bounds: gutter_bounds,
|
||||||
background: Some(self.style.gutter_background),
|
background: Some(self.style.gutter_background),
|
||||||
|
@ -469,7 +468,7 @@ impl EditorElement {
|
||||||
corner_radius: 0.,
|
corner_radius: 0.,
|
||||||
});
|
});
|
||||||
|
|
||||||
if let EditorMode::Full = editor.mode {
|
if let EditorMode::Full = layout.mode {
|
||||||
let mut active_rows = layout.active_rows.iter().peekable();
|
let mut active_rows = layout.active_rows.iter().peekable();
|
||||||
while let Some((start_row, contains_non_empty_selection)) = active_rows.next() {
|
while let Some((start_row, contains_non_empty_selection)) = active_rows.next() {
|
||||||
let mut end_row = *start_row;
|
let mut end_row = *start_row;
|
||||||
|
@ -753,7 +752,7 @@ impl EditorElement {
|
||||||
.snapshot
|
.snapshot
|
||||||
.chars_at(cursor_position)
|
.chars_at(cursor_position)
|
||||||
.next()
|
.next()
|
||||||
.and_then(|character| {
|
.and_then(|(character, _)| {
|
||||||
let font_id =
|
let font_id =
|
||||||
cursor_row_layout.font_for_index(cursor_column)?;
|
cursor_row_layout.font_for_index(cursor_column)?;
|
||||||
let text = character.to_string();
|
let text = character.to_string();
|
||||||
|
@ -910,6 +909,119 @@ impl EditorElement {
|
||||||
cx.scene.pop_layer();
|
cx.scene.pop_layer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn paint_scrollbar(&mut self, bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) {
|
||||||
|
enum ScrollbarMouseHandlers {}
|
||||||
|
if layout.mode != EditorMode::Full {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let view = self.view.clone();
|
||||||
|
let style = &self.style.theme.scrollbar;
|
||||||
|
|
||||||
|
let top = bounds.min_y();
|
||||||
|
let bottom = bounds.max_y();
|
||||||
|
let right = bounds.max_x();
|
||||||
|
let left = right - style.width;
|
||||||
|
let row_range = &layout.scrollbar_row_range;
|
||||||
|
let max_row = layout.max_row as f32 + (row_range.end - row_range.start);
|
||||||
|
|
||||||
|
let mut height = bounds.height();
|
||||||
|
let mut first_row_y_offset = 0.0;
|
||||||
|
|
||||||
|
// Impose a minimum height on the scrollbar thumb
|
||||||
|
let min_thumb_height =
|
||||||
|
style.min_height_factor * cx.font_cache.line_height(self.style.text.font_size);
|
||||||
|
let thumb_height = (row_range.end - row_range.start) * height / max_row;
|
||||||
|
if thumb_height < min_thumb_height {
|
||||||
|
first_row_y_offset = (min_thumb_height - thumb_height) / 2.0;
|
||||||
|
height -= min_thumb_height - thumb_height;
|
||||||
|
}
|
||||||
|
|
||||||
|
let y_for_row = |row: f32| -> f32 { top + first_row_y_offset + row * height / max_row };
|
||||||
|
|
||||||
|
let thumb_top = y_for_row(row_range.start) - first_row_y_offset;
|
||||||
|
let thumb_bottom = y_for_row(row_range.end) + first_row_y_offset;
|
||||||
|
let track_bounds = RectF::from_points(vec2f(left, top), vec2f(right, bottom));
|
||||||
|
let thumb_bounds = RectF::from_points(vec2f(left, thumb_top), vec2f(right, thumb_bottom));
|
||||||
|
|
||||||
|
if layout.show_scrollbars {
|
||||||
|
cx.scene.push_quad(Quad {
|
||||||
|
bounds: track_bounds,
|
||||||
|
border: style.track.border,
|
||||||
|
background: style.track.background_color,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
cx.scene.push_quad(Quad {
|
||||||
|
bounds: thumb_bounds,
|
||||||
|
border: style.thumb.border,
|
||||||
|
background: style.thumb.background_color,
|
||||||
|
corner_radius: style.thumb.corner_radius,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.scene.push_cursor_region(CursorRegion {
|
||||||
|
bounds: track_bounds,
|
||||||
|
style: CursorStyle::Arrow,
|
||||||
|
});
|
||||||
|
cx.scene.push_mouse_region(
|
||||||
|
MouseRegion::new::<ScrollbarMouseHandlers>(view.id(), view.id(), track_bounds)
|
||||||
|
.on_move({
|
||||||
|
let view = view.clone();
|
||||||
|
move |_, cx| {
|
||||||
|
if let Some(view) = view.upgrade(cx.deref_mut()) {
|
||||||
|
view.update(cx.deref_mut(), |view, cx| {
|
||||||
|
view.make_scrollbar_visible(cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_down(MouseButton::Left, {
|
||||||
|
let view = view.clone();
|
||||||
|
let row_range = row_range.clone();
|
||||||
|
move |e, cx| {
|
||||||
|
let y = e.position.y();
|
||||||
|
if let Some(view) = view.upgrade(cx.deref_mut()) {
|
||||||
|
view.update(cx.deref_mut(), |view, cx| {
|
||||||
|
if y < thumb_top || thumb_bottom < y {
|
||||||
|
let center_row =
|
||||||
|
((y - top) * max_row as f32 / height).round() as u32;
|
||||||
|
let top_row = center_row.saturating_sub(
|
||||||
|
(row_range.end - row_range.start) as u32 / 2,
|
||||||
|
);
|
||||||
|
let mut position = view.scroll_position(cx);
|
||||||
|
position.set_y(top_row as f32);
|
||||||
|
view.set_scroll_position(position, cx);
|
||||||
|
} else {
|
||||||
|
view.make_scrollbar_visible(cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_drag(MouseButton::Left, {
|
||||||
|
let view = view.clone();
|
||||||
|
move |e, cx| {
|
||||||
|
let y = e.prev_mouse_position.y();
|
||||||
|
let new_y = e.position.y();
|
||||||
|
if thumb_top < y && y < thumb_bottom {
|
||||||
|
if let Some(view) = view.upgrade(cx.deref_mut()) {
|
||||||
|
view.update(cx.deref_mut(), |view, cx| {
|
||||||
|
let mut position = view.scroll_position(cx);
|
||||||
|
position.set_y(
|
||||||
|
position.y() + (new_y - y) * (max_row as f32) / height,
|
||||||
|
);
|
||||||
|
if position.y() < 0.0 {
|
||||||
|
position.set_y(0.);
|
||||||
|
}
|
||||||
|
view.set_scroll_position(position, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn paint_highlighted_range(
|
fn paint_highlighted_range(
|
||||||
&self,
|
&self,
|
||||||
|
@ -1470,13 +1582,11 @@ impl Element for EditorElement {
|
||||||
// The scroll position is a fractional point, the whole number of which represents
|
// The scroll position is a fractional point, the whole number of which represents
|
||||||
// the top of the window in terms of display rows.
|
// the top of the window in terms of display rows.
|
||||||
let start_row = scroll_position.y() as u32;
|
let start_row = scroll_position.y() as u32;
|
||||||
let scroll_top = scroll_position.y() * line_height;
|
let visible_row_count = (size.y() / line_height).ceil() as u32;
|
||||||
|
let max_row = snapshot.max_point().row();
|
||||||
|
|
||||||
// Add 1 to ensure selections bleed off screen
|
// Add 1 to ensure selections bleed off screen
|
||||||
let end_row = 1 + cmp::min(
|
let end_row = 1 + cmp::min(start_row + visible_row_count, max_row);
|
||||||
((scroll_top + size.y()) / line_height).ceil() as u32,
|
|
||||||
snapshot.max_point().row(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let start_anchor = if start_row == 0 {
|
let start_anchor = if start_row == 0 {
|
||||||
Anchor::min()
|
Anchor::min()
|
||||||
|
@ -1485,7 +1595,7 @@ impl Element for EditorElement {
|
||||||
.buffer_snapshot
|
.buffer_snapshot
|
||||||
.anchor_before(DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left))
|
.anchor_before(DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left))
|
||||||
};
|
};
|
||||||
let end_anchor = if end_row > snapshot.max_point().row() {
|
let end_anchor = if end_row > max_row {
|
||||||
Anchor::max()
|
Anchor::max()
|
||||||
} else {
|
} else {
|
||||||
snapshot
|
snapshot
|
||||||
|
@ -1497,6 +1607,7 @@ impl Element for EditorElement {
|
||||||
let mut active_rows = BTreeMap::new();
|
let mut active_rows = BTreeMap::new();
|
||||||
let mut highlighted_rows = None;
|
let mut highlighted_rows = None;
|
||||||
let mut highlighted_ranges = Vec::new();
|
let mut highlighted_ranges = Vec::new();
|
||||||
|
let mut show_scrollbars = false;
|
||||||
self.update_view(cx.app, |view, cx| {
|
self.update_view(cx.app, |view, cx| {
|
||||||
let display_map = view.display_map.update(cx, |map, cx| map.snapshot(cx));
|
let display_map = view.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||||
|
|
||||||
|
@ -1557,6 +1668,8 @@ impl Element for EditorElement {
|
||||||
.collect(),
|
.collect(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
show_scrollbars = view.show_scrollbars();
|
||||||
});
|
});
|
||||||
|
|
||||||
let line_number_layouts =
|
let line_number_layouts =
|
||||||
|
@ -1567,6 +1680,9 @@ impl Element for EditorElement {
|
||||||
.git_diff_hunks_in_range(start_row..end_row)
|
.git_diff_hunks_in_range(start_row..end_row)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
let scrollbar_row_range =
|
||||||
|
scroll_position.y()..(scroll_position.y() + visible_row_count as f32);
|
||||||
|
|
||||||
let mut max_visible_line_width = 0.0;
|
let mut max_visible_line_width = 0.0;
|
||||||
let line_layouts = self.layout_lines(start_row..end_row, &snapshot, cx);
|
let line_layouts = self.layout_lines(start_row..end_row, &snapshot, cx);
|
||||||
for line in &line_layouts {
|
for line in &line_layouts {
|
||||||
|
@ -1600,10 +1716,9 @@ impl Element for EditorElement {
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
let max_row = snapshot.max_point().row();
|
|
||||||
let scroll_max = vec2f(
|
let scroll_max = vec2f(
|
||||||
((scroll_width - text_size.x()) / em_width).max(0.0),
|
((scroll_width - text_size.x()) / em_width).max(0.0),
|
||||||
max_row.saturating_sub(1) as f32,
|
max_row as f32,
|
||||||
);
|
);
|
||||||
|
|
||||||
self.update_view(cx.app, |view, cx| {
|
self.update_view(cx.app, |view, cx| {
|
||||||
|
@ -1630,6 +1745,7 @@ impl Element for EditorElement {
|
||||||
let mut context_menu = None;
|
let mut context_menu = None;
|
||||||
let mut code_actions_indicator = None;
|
let mut code_actions_indicator = None;
|
||||||
let mut hover = None;
|
let mut hover = None;
|
||||||
|
let mut mode = EditorMode::Full;
|
||||||
cx.render(&self.view.upgrade(cx).unwrap(), |view, cx| {
|
cx.render(&self.view.upgrade(cx).unwrap(), |view, cx| {
|
||||||
let newest_selection_head = view
|
let newest_selection_head = view
|
||||||
.selections
|
.selections
|
||||||
|
@ -1651,6 +1767,7 @@ impl Element for EditorElement {
|
||||||
|
|
||||||
let visible_rows = start_row..start_row + line_layouts.len() as u32;
|
let visible_rows = start_row..start_row + line_layouts.len() as u32;
|
||||||
hover = view.hover_state.render(&snapshot, &style, visible_rows, cx);
|
hover = view.hover_state.render(&snapshot, &style, visible_rows, cx);
|
||||||
|
mode = view.mode;
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some((_, context_menu)) = context_menu.as_mut() {
|
if let Some((_, context_menu)) = context_menu.as_mut() {
|
||||||
|
@ -1698,6 +1815,7 @@ impl Element for EditorElement {
|
||||||
(
|
(
|
||||||
size,
|
size,
|
||||||
LayoutState {
|
LayoutState {
|
||||||
|
mode,
|
||||||
position_map: Arc::new(PositionMap {
|
position_map: Arc::new(PositionMap {
|
||||||
size,
|
size,
|
||||||
scroll_max,
|
scroll_max,
|
||||||
|
@ -1710,6 +1828,9 @@ impl Element for EditorElement {
|
||||||
gutter_size,
|
gutter_size,
|
||||||
gutter_padding,
|
gutter_padding,
|
||||||
text_size,
|
text_size,
|
||||||
|
scrollbar_row_range,
|
||||||
|
show_scrollbars,
|
||||||
|
max_row,
|
||||||
gutter_margin,
|
gutter_margin,
|
||||||
active_rows,
|
active_rows,
|
||||||
highlighted_rows,
|
highlighted_rows,
|
||||||
|
@ -1757,11 +1878,12 @@ impl Element for EditorElement {
|
||||||
}
|
}
|
||||||
self.paint_text(text_bounds, visible_bounds, layout, cx);
|
self.paint_text(text_bounds, visible_bounds, layout, cx);
|
||||||
|
|
||||||
|
cx.scene.push_layer(Some(bounds));
|
||||||
if !layout.blocks.is_empty() {
|
if !layout.blocks.is_empty() {
|
||||||
cx.scene.push_layer(Some(bounds));
|
|
||||||
self.paint_blocks(bounds, visible_bounds, layout, cx);
|
self.paint_blocks(bounds, visible_bounds, layout, cx);
|
||||||
cx.scene.pop_layer();
|
|
||||||
}
|
}
|
||||||
|
self.paint_scrollbar(bounds, layout, cx);
|
||||||
|
cx.scene.pop_layer();
|
||||||
|
|
||||||
cx.scene.pop_layer();
|
cx.scene.pop_layer();
|
||||||
}
|
}
|
||||||
|
@ -1847,12 +1969,16 @@ pub struct LayoutState {
|
||||||
gutter_padding: f32,
|
gutter_padding: f32,
|
||||||
gutter_margin: f32,
|
gutter_margin: f32,
|
||||||
text_size: Vector2F,
|
text_size: Vector2F,
|
||||||
|
mode: EditorMode,
|
||||||
active_rows: BTreeMap<u32, bool>,
|
active_rows: BTreeMap<u32, bool>,
|
||||||
highlighted_rows: Option<Range<u32>>,
|
highlighted_rows: Option<Range<u32>>,
|
||||||
line_number_layouts: Vec<Option<text_layout::Line>>,
|
line_number_layouts: Vec<Option<text_layout::Line>>,
|
||||||
blocks: Vec<BlockLayout>,
|
blocks: Vec<BlockLayout>,
|
||||||
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
|
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
|
||||||
selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
|
selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
|
||||||
|
scrollbar_row_range: Range<f32>,
|
||||||
|
show_scrollbars: bool,
|
||||||
|
max_row: u32,
|
||||||
context_menu: Option<(DisplayPoint, ElementBox)>,
|
context_menu: Option<(DisplayPoint, ElementBox)>,
|
||||||
diff_hunks: Vec<DiffHunk<u32>>,
|
diff_hunks: Vec<DiffHunk<u32>>,
|
||||||
code_actions_indicator: Option<(u32, ElementBox)>,
|
code_actions_indicator: Option<(u32, ElementBox)>,
|
||||||
|
|
|
@ -32,8 +32,9 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use crate::test::editor_lsp_test_context::EditorLspTestContext;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::test::EditorLspTestContext;
|
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use language::{BracketPair, Language, LanguageConfig};
|
use language::{BracketPair, Language, LanguageConfig};
|
||||||
|
|
||||||
|
|
|
@ -427,13 +427,13 @@ impl DiagnosticPopover {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use futures::StreamExt;
|
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
|
|
||||||
use language::{Diagnostic, DiagnosticSet};
|
use language::{Diagnostic, DiagnosticSet};
|
||||||
use project::HoverBlock;
|
use project::HoverBlock;
|
||||||
|
use smol::stream::StreamExt;
|
||||||
|
|
||||||
use crate::test::EditorLspTestContext;
|
use crate::test::editor_lsp_test_context::EditorLspTestContext;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|
|
@ -400,7 +400,7 @@ mod tests {
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use lsp::request::{GotoDefinition, GotoTypeDefinition};
|
use lsp::request::{GotoDefinition, GotoTypeDefinition};
|
||||||
|
|
||||||
use crate::test::EditorLspTestContext;
|
use crate::test::editor_lsp_test_context::EditorLspTestContext;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|
|
@ -70,8 +70,9 @@ pub fn deploy_context_menu(
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use crate::test::editor_lsp_test_context::EditorLspTestContext;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::test::EditorLspTestContext;
|
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
|
|
@ -102,6 +102,22 @@ pub fn line_beginning(
|
||||||
map: &DisplaySnapshot,
|
map: &DisplaySnapshot,
|
||||||
display_point: DisplayPoint,
|
display_point: DisplayPoint,
|
||||||
stop_at_soft_boundaries: bool,
|
stop_at_soft_boundaries: bool,
|
||||||
|
) -> DisplayPoint {
|
||||||
|
let point = display_point.to_point(map);
|
||||||
|
let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
|
||||||
|
let line_start = map.prev_line_boundary(point).1;
|
||||||
|
|
||||||
|
if stop_at_soft_boundaries && display_point != soft_line_start {
|
||||||
|
soft_line_start
|
||||||
|
} else {
|
||||||
|
line_start
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn indented_line_beginning(
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
display_point: DisplayPoint,
|
||||||
|
stop_at_soft_boundaries: bool,
|
||||||
) -> DisplayPoint {
|
) -> DisplayPoint {
|
||||||
let point = display_point.to_point(map);
|
let point = display_point.to_point(map);
|
||||||
let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
|
let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
|
||||||
|
@ -168,54 +184,79 @@ pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPo
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scans for a boundary from the start of each line preceding the given end point until a boundary
|
/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
|
||||||
/// is found, indicated by the given predicate returning true. The predicate is called with the
|
/// given predicate returning true. The predicate is called with the character to the left and right
|
||||||
/// character to the left and right of the candidate boundary location, and will be called with `\n`
|
/// of the candidate boundary location, and will be called with `\n` characters indicating the start
|
||||||
/// characters indicating the start or end of a line. If the predicate returns true multiple times
|
/// or end of a line.
|
||||||
/// on a line, the *rightmost* boundary is returned.
|
|
||||||
pub fn find_preceding_boundary(
|
pub fn find_preceding_boundary(
|
||||||
map: &DisplaySnapshot,
|
map: &DisplaySnapshot,
|
||||||
end: DisplayPoint,
|
from: DisplayPoint,
|
||||||
mut is_boundary: impl FnMut(char, char) -> bool,
|
mut is_boundary: impl FnMut(char, char) -> bool,
|
||||||
) -> DisplayPoint {
|
) -> DisplayPoint {
|
||||||
let mut point = end;
|
let mut start_column = 0;
|
||||||
loop {
|
let mut soft_wrap_row = from.row() + 1;
|
||||||
*point.column_mut() = 0;
|
|
||||||
if point.row() > 0 {
|
let mut prev = None;
|
||||||
if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
|
for (ch, point) in map.reverse_chars_at(from) {
|
||||||
*point.column_mut() = indent;
|
// Recompute soft_wrap_indent if the row has changed
|
||||||
|
if point.row() != soft_wrap_row {
|
||||||
|
soft_wrap_row = point.row();
|
||||||
|
|
||||||
|
if point.row() == 0 {
|
||||||
|
start_column = 0;
|
||||||
|
} else if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
|
||||||
|
start_column = indent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut boundary = None;
|
// If the current point is in the soft_wrap, skip comparing it
|
||||||
let mut prev_ch = if point.is_zero() { None } else { Some('\n') };
|
if point.column() < start_column {
|
||||||
for ch in map.chars_at(point) {
|
continue;
|
||||||
if point >= end {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(prev_ch) = prev_ch {
|
|
||||||
if is_boundary(prev_ch, ch) {
|
|
||||||
boundary = Some(point);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ch == '\n' {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
prev_ch = Some(ch);
|
|
||||||
*point.column_mut() += ch.len_utf8() as u32;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(boundary) = boundary {
|
if let Some((prev_ch, prev_point)) = prev {
|
||||||
return boundary;
|
if is_boundary(ch, prev_ch) {
|
||||||
} else if point.row() == 0 {
|
return prev_point;
|
||||||
return DisplayPoint::zero();
|
}
|
||||||
} else {
|
}
|
||||||
*point.row_mut() -= 1;
|
|
||||||
|
prev = Some((ch, point));
|
||||||
|
}
|
||||||
|
DisplayPoint::zero()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
|
||||||
|
/// given predicate returning true. The predicate is called with the character to the left and right
|
||||||
|
/// of the candidate boundary location, and will be called with `\n` characters indicating the start
|
||||||
|
/// or end of a line. If no boundary is found, the start of the line is returned.
|
||||||
|
pub fn find_preceding_boundary_in_line(
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
from: DisplayPoint,
|
||||||
|
mut is_boundary: impl FnMut(char, char) -> bool,
|
||||||
|
) -> DisplayPoint {
|
||||||
|
let mut start_column = 0;
|
||||||
|
if from.row() > 0 {
|
||||||
|
if let Some(indent) = map.soft_wrap_indent(from.row() - 1) {
|
||||||
|
start_column = indent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut prev = None;
|
||||||
|
for (ch, point) in map.reverse_chars_at(from) {
|
||||||
|
if let Some((prev_ch, prev_point)) = prev {
|
||||||
|
if is_boundary(ch, prev_ch) {
|
||||||
|
return prev_point;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ch == '\n' || point.column() < start_column {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
prev = Some((ch, point));
|
||||||
|
}
|
||||||
|
|
||||||
|
prev.map(|(_, point)| point).unwrap_or(from)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scans for a boundary following the given start point until a boundary is found, indicated by the
|
/// Scans for a boundary following the given start point until a boundary is found, indicated by the
|
||||||
|
@ -224,26 +265,48 @@ pub fn find_preceding_boundary(
|
||||||
/// or end of a line.
|
/// or end of a line.
|
||||||
pub fn find_boundary(
|
pub fn find_boundary(
|
||||||
map: &DisplaySnapshot,
|
map: &DisplaySnapshot,
|
||||||
mut point: DisplayPoint,
|
from: DisplayPoint,
|
||||||
mut is_boundary: impl FnMut(char, char) -> bool,
|
mut is_boundary: impl FnMut(char, char) -> bool,
|
||||||
) -> DisplayPoint {
|
) -> DisplayPoint {
|
||||||
let mut prev_ch = None;
|
let mut prev_ch = None;
|
||||||
for ch in map.chars_at(point) {
|
for (ch, point) in map.chars_at(from) {
|
||||||
if let Some(prev_ch) = prev_ch {
|
if let Some(prev_ch) = prev_ch {
|
||||||
if is_boundary(prev_ch, ch) {
|
if is_boundary(prev_ch, ch) {
|
||||||
break;
|
return map.clip_point(point, Bias::Right);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ch == '\n' {
|
|
||||||
*point.row_mut() += 1;
|
|
||||||
*point.column_mut() = 0;
|
|
||||||
} else {
|
|
||||||
*point.column_mut() += ch.len_utf8() as u32;
|
|
||||||
}
|
|
||||||
prev_ch = Some(ch);
|
prev_ch = Some(ch);
|
||||||
}
|
}
|
||||||
map.clip_point(point, Bias::Right)
|
map.clip_point(map.max_point(), Bias::Right)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scans for a boundary following the given start point until a boundary is found, indicated by the
|
||||||
|
/// given predicate returning true. The predicate is called with the character to the left and right
|
||||||
|
/// of the candidate boundary location, and will be called with `\n` characters indicating the start
|
||||||
|
/// or end of a line. If no boundary is found, the end of the line is returned
|
||||||
|
pub fn find_boundary_in_line(
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
from: DisplayPoint,
|
||||||
|
mut is_boundary: impl FnMut(char, char) -> bool,
|
||||||
|
) -> DisplayPoint {
|
||||||
|
let mut prev = None;
|
||||||
|
for (ch, point) in map.chars_at(from) {
|
||||||
|
if let Some((prev_ch, _)) = prev {
|
||||||
|
if is_boundary(prev_ch, ch) {
|
||||||
|
return map.clip_point(point, Bias::Right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prev = Some((ch, point));
|
||||||
|
|
||||||
|
if ch == '\n' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the last position checked so that we give a point right before the newline or eof.
|
||||||
|
map.clip_point(prev.map(|(_, point)| point).unwrap_or(from), Bias::Right)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
|
pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
|
||||||
|
|
|
@ -1,28 +1,14 @@
|
||||||
|
pub mod editor_lsp_test_context;
|
||||||
|
pub mod editor_test_context;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
|
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
|
||||||
multi_buffer::ToPointUtf16,
|
DisplayPoint, Editor, EditorMode, MultiBuffer,
|
||||||
AnchorRangeExt, Autoscroll, DisplayPoint, Editor, EditorMode, MultiBuffer, ToPoint,
|
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
|
||||||
use futures::{Future, StreamExt};
|
use gpui::{ModelHandle, ViewContext};
|
||||||
use gpui::{
|
|
||||||
json, keymap::Keystroke, AppContext, ModelContext, ModelHandle, ViewContext, ViewHandle,
|
use util::test::{marked_text_offsets, marked_text_ranges};
|
||||||
};
|
|
||||||
use indoc::indoc;
|
|
||||||
use language::{point_to_lsp, Buffer, BufferSnapshot, FakeLspAdapter, Language, LanguageConfig};
|
|
||||||
use lsp::{notification, request};
|
|
||||||
use project::Project;
|
|
||||||
use settings::Settings;
|
|
||||||
use std::{
|
|
||||||
any::TypeId,
|
|
||||||
ops::{Deref, DerefMut, Range},
|
|
||||||
sync::Arc,
|
|
||||||
};
|
|
||||||
use util::{
|
|
||||||
assert_set_eq, set_eq,
|
|
||||||
test::{generate_marked_text, marked_text_offsets, marked_text_ranges},
|
|
||||||
};
|
|
||||||
use workspace::{pane, AppState, Workspace, WorkspaceHandle};
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[ctor::ctor]
|
#[ctor::ctor]
|
||||||
|
@ -80,430 +66,3 @@ pub(crate) fn build_editor(
|
||||||
) -> Editor {
|
) -> Editor {
|
||||||
Editor::new(EditorMode::Full, buffer, None, None, cx)
|
Editor::new(EditorMode::Full, buffer, None, None, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct EditorTestContext<'a> {
|
|
||||||
pub cx: &'a mut gpui::TestAppContext,
|
|
||||||
pub window_id: usize,
|
|
||||||
pub editor: ViewHandle<Editor>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> EditorTestContext<'a> {
|
|
||||||
pub fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
|
|
||||||
let (window_id, editor) = cx.update(|cx| {
|
|
||||||
cx.set_global(Settings::test(cx));
|
|
||||||
crate::init(cx);
|
|
||||||
|
|
||||||
let (window_id, editor) = cx.add_window(Default::default(), |cx| {
|
|
||||||
build_editor(MultiBuffer::build_simple("", cx), cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
editor.update(cx, |_, cx| cx.focus_self());
|
|
||||||
|
|
||||||
(window_id, editor)
|
|
||||||
});
|
|
||||||
|
|
||||||
Self {
|
|
||||||
cx,
|
|
||||||
window_id,
|
|
||||||
editor,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn condition(
|
|
||||||
&self,
|
|
||||||
predicate: impl FnMut(&Editor, &AppContext) -> bool,
|
|
||||||
) -> impl Future<Output = ()> {
|
|
||||||
self.editor.condition(self.cx, predicate)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn editor<F, T>(&self, read: F) -> T
|
|
||||||
where
|
|
||||||
F: FnOnce(&Editor, &AppContext) -> T,
|
|
||||||
{
|
|
||||||
self.editor.read_with(self.cx, read)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_editor<F, T>(&mut self, update: F) -> T
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T,
|
|
||||||
{
|
|
||||||
self.editor.update(self.cx, update)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn multibuffer<F, T>(&self, read: F) -> T
|
|
||||||
where
|
|
||||||
F: FnOnce(&MultiBuffer, &AppContext) -> T,
|
|
||||||
{
|
|
||||||
self.editor(|editor, cx| read(editor.buffer().read(cx), cx))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_multibuffer<F, T>(&mut self, update: F) -> T
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut MultiBuffer, &mut ModelContext<MultiBuffer>) -> T,
|
|
||||||
{
|
|
||||||
self.update_editor(|editor, cx| editor.buffer().update(cx, update))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn buffer_text(&self) -> String {
|
|
||||||
self.multibuffer(|buffer, cx| buffer.snapshot(cx).text())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn buffer<F, T>(&self, read: F) -> T
|
|
||||||
where
|
|
||||||
F: FnOnce(&Buffer, &AppContext) -> T,
|
|
||||||
{
|
|
||||||
self.multibuffer(|multibuffer, cx| {
|
|
||||||
let buffer = multibuffer.as_singleton().unwrap().read(cx);
|
|
||||||
read(buffer, cx)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_buffer<F, T>(&mut self, update: F) -> T
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut Buffer, &mut ModelContext<Buffer>) -> T,
|
|
||||||
{
|
|
||||||
self.update_multibuffer(|multibuffer, cx| {
|
|
||||||
let buffer = multibuffer.as_singleton().unwrap();
|
|
||||||
buffer.update(cx, update)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn buffer_snapshot(&self) -> BufferSnapshot {
|
|
||||||
self.buffer(|buffer, _| buffer.snapshot())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn simulate_keystroke(&mut self, keystroke_text: &str) {
|
|
||||||
let keystroke = Keystroke::parse(keystroke_text).unwrap();
|
|
||||||
self.cx.dispatch_keystroke(self.window_id, keystroke, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn simulate_keystrokes<const COUNT: usize>(&mut self, keystroke_texts: [&str; COUNT]) {
|
|
||||||
for keystroke_text in keystroke_texts.into_iter() {
|
|
||||||
self.simulate_keystroke(keystroke_text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ranges(&self, marked_text: &str) -> Vec<Range<usize>> {
|
|
||||||
let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
|
|
||||||
assert_eq!(self.buffer_text(), unmarked_text);
|
|
||||||
ranges
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint {
|
|
||||||
let ranges = self.ranges(marked_text);
|
|
||||||
let snapshot = self
|
|
||||||
.editor
|
|
||||||
.update(self.cx, |editor, cx| editor.snapshot(cx));
|
|
||||||
ranges[0].start.to_display_point(&snapshot)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns anchors for the current buffer using `«` and `»`
|
|
||||||
pub fn text_anchor_range(&self, marked_text: &str) -> Range<language::Anchor> {
|
|
||||||
let ranges = self.ranges(marked_text);
|
|
||||||
let snapshot = self.buffer_snapshot();
|
|
||||||
snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Change the editor's text and selections using a string containing
|
|
||||||
/// embedded range markers that represent the ranges and directions of
|
|
||||||
/// each selection.
|
|
||||||
///
|
|
||||||
/// See the `util::test::marked_text_ranges` function for more information.
|
|
||||||
pub fn set_state(&mut self, marked_text: &str) {
|
|
||||||
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
|
|
||||||
self.editor.update(self.cx, |editor, cx| {
|
|
||||||
editor.set_text(unmarked_text, cx);
|
|
||||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
|
||||||
s.select_ranges(selection_ranges)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Make an assertion about the editor's text and the ranges and directions
|
|
||||||
/// of its selections using a string containing embedded range markers.
|
|
||||||
///
|
|
||||||
/// See the `util::test::marked_text_ranges` function for more information.
|
|
||||||
pub fn assert_editor_state(&mut self, marked_text: &str) {
|
|
||||||
let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true);
|
|
||||||
let buffer_text = self.buffer_text();
|
|
||||||
assert_eq!(
|
|
||||||
buffer_text, unmarked_text,
|
|
||||||
"Unmarked text doesn't match buffer text"
|
|
||||||
);
|
|
||||||
self.assert_selections(expected_selections, marked_text.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
|
|
||||||
let expected_ranges = self.ranges(marked_text);
|
|
||||||
let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
|
|
||||||
let snapshot = editor.snapshot(cx);
|
|
||||||
editor
|
|
||||||
.background_highlights
|
|
||||||
.get(&TypeId::of::<Tag>())
|
|
||||||
.map(|h| h.1.clone())
|
|
||||||
.unwrap_or_default()
|
|
||||||
.into_iter()
|
|
||||||
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
|
|
||||||
.collect()
|
|
||||||
});
|
|
||||||
assert_set_eq!(actual_ranges, expected_ranges);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
|
|
||||||
let expected_ranges = self.ranges(marked_text);
|
|
||||||
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
|
|
||||||
let actual_ranges: Vec<Range<usize>> = snapshot
|
|
||||||
.highlight_ranges::<Tag>()
|
|
||||||
.map(|ranges| ranges.as_ref().clone().1)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.into_iter()
|
|
||||||
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
|
|
||||||
.collect();
|
|
||||||
assert_set_eq!(actual_ranges, expected_ranges);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
|
|
||||||
let expected_marked_text =
|
|
||||||
generate_marked_text(&self.buffer_text(), &expected_selections, true);
|
|
||||||
self.assert_selections(expected_selections, expected_marked_text)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn assert_selections(
|
|
||||||
&mut self,
|
|
||||||
expected_selections: Vec<Range<usize>>,
|
|
||||||
expected_marked_text: String,
|
|
||||||
) {
|
|
||||||
let actual_selections = self
|
|
||||||
.editor
|
|
||||||
.read_with(self.cx, |editor, cx| editor.selections.all::<usize>(cx))
|
|
||||||
.into_iter()
|
|
||||||
.map(|s| {
|
|
||||||
if s.reversed {
|
|
||||||
s.end..s.start
|
|
||||||
} else {
|
|
||||||
s.start..s.end
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let actual_marked_text =
|
|
||||||
generate_marked_text(&self.buffer_text(), &actual_selections, true);
|
|
||||||
if expected_selections != actual_selections {
|
|
||||||
panic!(
|
|
||||||
indoc! {"
|
|
||||||
Editor has unexpected selections.
|
|
||||||
|
|
||||||
Expected selections:
|
|
||||||
{}
|
|
||||||
|
|
||||||
Actual selections:
|
|
||||||
{}
|
|
||||||
"},
|
|
||||||
expected_marked_text, actual_marked_text,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Deref for EditorTestContext<'a> {
|
|
||||||
type Target = gpui::TestAppContext;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
self.cx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> DerefMut for EditorTestContext<'a> {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.cx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct EditorLspTestContext<'a> {
|
|
||||||
pub cx: EditorTestContext<'a>,
|
|
||||||
pub lsp: lsp::FakeLanguageServer,
|
|
||||||
pub workspace: ViewHandle<Workspace>,
|
|
||||||
pub buffer_lsp_url: lsp::Url,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> EditorLspTestContext<'a> {
|
|
||||||
pub async fn new(
|
|
||||||
mut language: Language,
|
|
||||||
capabilities: lsp::ServerCapabilities,
|
|
||||||
cx: &'a mut gpui::TestAppContext,
|
|
||||||
) -> EditorLspTestContext<'a> {
|
|
||||||
use json::json;
|
|
||||||
|
|
||||||
cx.update(|cx| {
|
|
||||||
crate::init(cx);
|
|
||||||
pane::init(cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
let params = cx.update(AppState::test);
|
|
||||||
|
|
||||||
let file_name = format!(
|
|
||||||
"file.{}",
|
|
||||||
language
|
|
||||||
.path_suffixes()
|
|
||||||
.first()
|
|
||||||
.unwrap_or(&"txt".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut fake_servers = language
|
|
||||||
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
|
||||||
capabilities,
|
|
||||||
..Default::default()
|
|
||||||
}))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let project = Project::test(params.fs.clone(), [], cx).await;
|
|
||||||
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
|
|
||||||
|
|
||||||
params
|
|
||||||
.fs
|
|
||||||
.as_fake()
|
|
||||||
.insert_tree("/root", json!({ "dir": { file_name: "" }}))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let (window_id, workspace) =
|
|
||||||
cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
|
|
||||||
project
|
|
||||||
.update(cx, |project, cx| {
|
|
||||||
project.find_or_create_local_worktree("/root", true, cx)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
|
|
||||||
let item = workspace
|
|
||||||
.update(cx, |workspace, cx| workspace.open_path(file, true, cx))
|
|
||||||
.await
|
|
||||||
.expect("Could not open test file");
|
|
||||||
|
|
||||||
let editor = cx.update(|cx| {
|
|
||||||
item.act_as::<Editor>(cx)
|
|
||||||
.expect("Opened test file wasn't an editor")
|
|
||||||
});
|
|
||||||
editor.update(cx, |_, cx| cx.focus_self());
|
|
||||||
|
|
||||||
let lsp = fake_servers.next().await.unwrap();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
cx: EditorTestContext {
|
|
||||||
cx,
|
|
||||||
window_id,
|
|
||||||
editor,
|
|
||||||
},
|
|
||||||
lsp,
|
|
||||||
workspace,
|
|
||||||
buffer_lsp_url: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn new_rust(
|
|
||||||
capabilities: lsp::ServerCapabilities,
|
|
||||||
cx: &'a mut gpui::TestAppContext,
|
|
||||||
) -> EditorLspTestContext<'a> {
|
|
||||||
let language = Language::new(
|
|
||||||
LanguageConfig {
|
|
||||||
name: "Rust".into(),
|
|
||||||
path_suffixes: vec!["rs".to_string()],
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
Some(tree_sitter_rust::language()),
|
|
||||||
);
|
|
||||||
|
|
||||||
Self::new(language, capabilities, cx).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// Constructs lsp range using a marked string with '[', ']' range delimiters
|
|
||||||
pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
|
|
||||||
let ranges = self.ranges(marked_text);
|
|
||||||
self.to_lsp_range(ranges[0].clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
|
|
||||||
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
|
|
||||||
let start_point = range.start.to_point(&snapshot.buffer_snapshot);
|
|
||||||
let end_point = range.end.to_point(&snapshot.buffer_snapshot);
|
|
||||||
|
|
||||||
self.editor(|editor, cx| {
|
|
||||||
let buffer = editor.buffer().read(cx);
|
|
||||||
let start = point_to_lsp(
|
|
||||||
buffer
|
|
||||||
.point_to_buffer_offset(start_point, cx)
|
|
||||||
.unwrap()
|
|
||||||
.1
|
|
||||||
.to_point_utf16(&buffer.read(cx)),
|
|
||||||
);
|
|
||||||
let end = point_to_lsp(
|
|
||||||
buffer
|
|
||||||
.point_to_buffer_offset(end_point, cx)
|
|
||||||
.unwrap()
|
|
||||||
.1
|
|
||||||
.to_point_utf16(&buffer.read(cx)),
|
|
||||||
);
|
|
||||||
|
|
||||||
lsp::Range { start, end }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
|
|
||||||
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
|
|
||||||
let point = offset.to_point(&snapshot.buffer_snapshot);
|
|
||||||
|
|
||||||
self.editor(|editor, cx| {
|
|
||||||
let buffer = editor.buffer().read(cx);
|
|
||||||
point_to_lsp(
|
|
||||||
buffer
|
|
||||||
.point_to_buffer_offset(point, cx)
|
|
||||||
.unwrap()
|
|
||||||
.1
|
|
||||||
.to_point_utf16(&buffer.read(cx)),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_workspace<F, T>(&mut self, update: F) -> T
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
|
|
||||||
{
|
|
||||||
self.workspace.update(self.cx.cx, update)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_request<T, F, Fut>(
|
|
||||||
&self,
|
|
||||||
mut handler: F,
|
|
||||||
) -> futures::channel::mpsc::UnboundedReceiver<()>
|
|
||||||
where
|
|
||||||
T: 'static + request::Request,
|
|
||||||
T::Params: 'static + Send,
|
|
||||||
F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
|
|
||||||
Fut: 'static + Send + Future<Output = Result<T::Result>>,
|
|
||||||
{
|
|
||||||
let url = self.buffer_lsp_url.clone();
|
|
||||||
self.lsp.handle_request::<T, _, _>(move |params, cx| {
|
|
||||||
let url = url.clone();
|
|
||||||
handler(url, params, cx)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn notify<T: notification::Notification>(&self, params: T::Params) {
|
|
||||||
self.lsp.notify::<T>(params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Deref for EditorLspTestContext<'a> {
|
|
||||||
type Target = EditorTestContext<'a>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.cx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> DerefMut for EditorLspTestContext<'a> {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.cx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
208
crates/editor/src/test/editor_lsp_test_context.rs
Normal file
208
crates/editor/src/test/editor_lsp_test_context.rs
Normal file
|
@ -0,0 +1,208 @@
|
||||||
|
use std::{
|
||||||
|
ops::{Deref, DerefMut, Range},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use futures::Future;
|
||||||
|
use gpui::{json, ViewContext, ViewHandle};
|
||||||
|
use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig};
|
||||||
|
use lsp::{notification, request};
|
||||||
|
use project::Project;
|
||||||
|
use smol::stream::StreamExt;
|
||||||
|
use workspace::{pane, AppState, Workspace, WorkspaceHandle};
|
||||||
|
|
||||||
|
use crate::{multi_buffer::ToPointUtf16, Editor, ToPoint};
|
||||||
|
|
||||||
|
use super::editor_test_context::EditorTestContext;
|
||||||
|
|
||||||
|
pub struct EditorLspTestContext<'a> {
|
||||||
|
pub cx: EditorTestContext<'a>,
|
||||||
|
pub lsp: lsp::FakeLanguageServer,
|
||||||
|
pub workspace: ViewHandle<Workspace>,
|
||||||
|
pub buffer_lsp_url: lsp::Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> EditorLspTestContext<'a> {
|
||||||
|
pub async fn new(
|
||||||
|
mut language: Language,
|
||||||
|
capabilities: lsp::ServerCapabilities,
|
||||||
|
cx: &'a mut gpui::TestAppContext,
|
||||||
|
) -> EditorLspTestContext<'a> {
|
||||||
|
use json::json;
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
crate::init(cx);
|
||||||
|
pane::init(cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
let params = cx.update(AppState::test);
|
||||||
|
|
||||||
|
let file_name = format!(
|
||||||
|
"file.{}",
|
||||||
|
language
|
||||||
|
.path_suffixes()
|
||||||
|
.first()
|
||||||
|
.unwrap_or(&"txt".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut fake_servers = language
|
||||||
|
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||||
|
capabilities,
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let project = Project::test(params.fs.clone(), [], cx).await;
|
||||||
|
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
|
||||||
|
|
||||||
|
params
|
||||||
|
.fs
|
||||||
|
.as_fake()
|
||||||
|
.insert_tree("/root", json!({ "dir": { file_name: "" }}))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let (window_id, workspace) =
|
||||||
|
cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
|
||||||
|
project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.find_or_create_local_worktree("/root", true, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
|
||||||
|
let item = workspace
|
||||||
|
.update(cx, |workspace, cx| workspace.open_path(file, true, cx))
|
||||||
|
.await
|
||||||
|
.expect("Could not open test file");
|
||||||
|
|
||||||
|
let editor = cx.update(|cx| {
|
||||||
|
item.act_as::<Editor>(cx)
|
||||||
|
.expect("Opened test file wasn't an editor")
|
||||||
|
});
|
||||||
|
editor.update(cx, |_, cx| cx.focus_self());
|
||||||
|
|
||||||
|
let lsp = fake_servers.next().await.unwrap();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
cx: EditorTestContext {
|
||||||
|
cx,
|
||||||
|
window_id,
|
||||||
|
editor,
|
||||||
|
},
|
||||||
|
lsp,
|
||||||
|
workspace,
|
||||||
|
buffer_lsp_url: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn new_rust(
|
||||||
|
capabilities: lsp::ServerCapabilities,
|
||||||
|
cx: &'a mut gpui::TestAppContext,
|
||||||
|
) -> EditorLspTestContext<'a> {
|
||||||
|
let language = Language::new(
|
||||||
|
LanguageConfig {
|
||||||
|
name: "Rust".into(),
|
||||||
|
path_suffixes: vec!["rs".to_string()],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Some(tree_sitter_rust::language()),
|
||||||
|
);
|
||||||
|
|
||||||
|
Self::new(language, capabilities, cx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructs lsp range using a marked string with '[', ']' range delimiters
|
||||||
|
pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
|
||||||
|
let ranges = self.ranges(marked_text);
|
||||||
|
self.to_lsp_range(ranges[0].clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
|
||||||
|
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
|
||||||
|
let start_point = range.start.to_point(&snapshot.buffer_snapshot);
|
||||||
|
let end_point = range.end.to_point(&snapshot.buffer_snapshot);
|
||||||
|
|
||||||
|
self.editor(|editor, cx| {
|
||||||
|
let buffer = editor.buffer().read(cx);
|
||||||
|
let start = point_to_lsp(
|
||||||
|
buffer
|
||||||
|
.point_to_buffer_offset(start_point, cx)
|
||||||
|
.unwrap()
|
||||||
|
.1
|
||||||
|
.to_point_utf16(&buffer.read(cx)),
|
||||||
|
);
|
||||||
|
let end = point_to_lsp(
|
||||||
|
buffer
|
||||||
|
.point_to_buffer_offset(end_point, cx)
|
||||||
|
.unwrap()
|
||||||
|
.1
|
||||||
|
.to_point_utf16(&buffer.read(cx)),
|
||||||
|
);
|
||||||
|
|
||||||
|
lsp::Range { start, end }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
|
||||||
|
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
|
||||||
|
let point = offset.to_point(&snapshot.buffer_snapshot);
|
||||||
|
|
||||||
|
self.editor(|editor, cx| {
|
||||||
|
let buffer = editor.buffer().read(cx);
|
||||||
|
point_to_lsp(
|
||||||
|
buffer
|
||||||
|
.point_to_buffer_offset(point, cx)
|
||||||
|
.unwrap()
|
||||||
|
.1
|
||||||
|
.to_point_utf16(&buffer.read(cx)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_workspace<F, T>(&mut self, update: F) -> T
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
|
||||||
|
{
|
||||||
|
self.workspace.update(self.cx.cx, update)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_request<T, F, Fut>(
|
||||||
|
&self,
|
||||||
|
mut handler: F,
|
||||||
|
) -> futures::channel::mpsc::UnboundedReceiver<()>
|
||||||
|
where
|
||||||
|
T: 'static + request::Request,
|
||||||
|
T::Params: 'static + Send,
|
||||||
|
F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
|
||||||
|
Fut: 'static + Send + Future<Output = Result<T::Result>>,
|
||||||
|
{
|
||||||
|
let url = self.buffer_lsp_url.clone();
|
||||||
|
self.lsp.handle_request::<T, _, _>(move |params, cx| {
|
||||||
|
let url = url.clone();
|
||||||
|
handler(url, params, cx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn notify<T: notification::Notification>(&self, params: T::Params) {
|
||||||
|
self.lsp.notify::<T>(params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Deref for EditorLspTestContext<'a> {
|
||||||
|
type Target = EditorTestContext<'a>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.cx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> DerefMut for EditorLspTestContext<'a> {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.cx
|
||||||
|
}
|
||||||
|
}
|
273
crates/editor/src/test/editor_test_context.rs
Normal file
273
crates/editor/src/test/editor_test_context.rs
Normal file
|
@ -0,0 +1,273 @@
|
||||||
|
use std::{
|
||||||
|
any::TypeId,
|
||||||
|
ops::{Deref, DerefMut, Range},
|
||||||
|
};
|
||||||
|
|
||||||
|
use futures::Future;
|
||||||
|
use indoc::indoc;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
|
||||||
|
};
|
||||||
|
use gpui::{keymap::Keystroke, AppContext, ContextHandle, ModelContext, ViewContext, ViewHandle};
|
||||||
|
use language::{Buffer, BufferSnapshot};
|
||||||
|
use settings::Settings;
|
||||||
|
use util::{
|
||||||
|
assert_set_eq,
|
||||||
|
test::{generate_marked_text, marked_text_ranges},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::build_editor;
|
||||||
|
|
||||||
|
pub struct EditorTestContext<'a> {
|
||||||
|
pub cx: &'a mut gpui::TestAppContext,
|
||||||
|
pub window_id: usize,
|
||||||
|
pub editor: ViewHandle<Editor>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> EditorTestContext<'a> {
|
||||||
|
pub fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
|
||||||
|
let (window_id, editor) = cx.update(|cx| {
|
||||||
|
cx.set_global(Settings::test(cx));
|
||||||
|
crate::init(cx);
|
||||||
|
|
||||||
|
let (window_id, editor) = cx.add_window(Default::default(), |cx| {
|
||||||
|
build_editor(MultiBuffer::build_simple("", cx), cx)
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.update(cx, |_, cx| cx.focus_self());
|
||||||
|
|
||||||
|
(window_id, editor)
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
cx,
|
||||||
|
window_id,
|
||||||
|
editor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn condition(
|
||||||
|
&self,
|
||||||
|
predicate: impl FnMut(&Editor, &AppContext) -> bool,
|
||||||
|
) -> impl Future<Output = ()> {
|
||||||
|
self.editor.condition(self.cx, predicate)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn editor<F, T>(&self, read: F) -> T
|
||||||
|
where
|
||||||
|
F: FnOnce(&Editor, &AppContext) -> T,
|
||||||
|
{
|
||||||
|
self.editor.read_with(self.cx, read)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_editor<F, T>(&mut self, update: F) -> T
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T,
|
||||||
|
{
|
||||||
|
self.editor.update(self.cx, update)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn multibuffer<F, T>(&self, read: F) -> T
|
||||||
|
where
|
||||||
|
F: FnOnce(&MultiBuffer, &AppContext) -> T,
|
||||||
|
{
|
||||||
|
self.editor(|editor, cx| read(editor.buffer().read(cx), cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_multibuffer<F, T>(&mut self, update: F) -> T
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut MultiBuffer, &mut ModelContext<MultiBuffer>) -> T,
|
||||||
|
{
|
||||||
|
self.update_editor(|editor, cx| editor.buffer().update(cx, update))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn buffer_text(&self) -> String {
|
||||||
|
self.multibuffer(|buffer, cx| buffer.snapshot(cx).text())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn buffer<F, T>(&self, read: F) -> T
|
||||||
|
where
|
||||||
|
F: FnOnce(&Buffer, &AppContext) -> T,
|
||||||
|
{
|
||||||
|
self.multibuffer(|multibuffer, cx| {
|
||||||
|
let buffer = multibuffer.as_singleton().unwrap().read(cx);
|
||||||
|
read(buffer, cx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_buffer<F, T>(&mut self, update: F) -> T
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut Buffer, &mut ModelContext<Buffer>) -> T,
|
||||||
|
{
|
||||||
|
self.update_multibuffer(|multibuffer, cx| {
|
||||||
|
let buffer = multibuffer.as_singleton().unwrap();
|
||||||
|
buffer.update(cx, update)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn buffer_snapshot(&self) -> BufferSnapshot {
|
||||||
|
self.buffer(|buffer, _| buffer.snapshot())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn simulate_keystroke(&mut self, keystroke_text: &str) -> ContextHandle {
|
||||||
|
let keystroke_under_test_handle =
|
||||||
|
self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text));
|
||||||
|
let keystroke = Keystroke::parse(keystroke_text).unwrap();
|
||||||
|
self.cx.dispatch_keystroke(self.window_id, keystroke, false);
|
||||||
|
keystroke_under_test_handle
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn simulate_keystrokes<const COUNT: usize>(
|
||||||
|
&mut self,
|
||||||
|
keystroke_texts: [&str; COUNT],
|
||||||
|
) -> ContextHandle {
|
||||||
|
let keystrokes_under_test_handle =
|
||||||
|
self.add_assertion_context(format!("Simulated Keystrokes: {:?}", keystroke_texts));
|
||||||
|
for keystroke_text in keystroke_texts.into_iter() {
|
||||||
|
self.simulate_keystroke(keystroke_text);
|
||||||
|
}
|
||||||
|
keystrokes_under_test_handle
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ranges(&self, marked_text: &str) -> Vec<Range<usize>> {
|
||||||
|
let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
|
||||||
|
assert_eq!(self.buffer_text(), unmarked_text);
|
||||||
|
ranges
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint {
|
||||||
|
let ranges = self.ranges(marked_text);
|
||||||
|
let snapshot = self
|
||||||
|
.editor
|
||||||
|
.update(self.cx, |editor, cx| editor.snapshot(cx));
|
||||||
|
ranges[0].start.to_display_point(&snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns anchors for the current buffer using `«` and `»`
|
||||||
|
pub fn text_anchor_range(&self, marked_text: &str) -> Range<language::Anchor> {
|
||||||
|
let ranges = self.ranges(marked_text);
|
||||||
|
let snapshot = self.buffer_snapshot();
|
||||||
|
snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change the editor's text and selections using a string containing
|
||||||
|
/// embedded range markers that represent the ranges and directions of
|
||||||
|
/// each selection.
|
||||||
|
///
|
||||||
|
/// See the `util::test::marked_text_ranges` function for more information.
|
||||||
|
pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
|
||||||
|
let _state_context = self.add_assertion_context(format!(
|
||||||
|
"Editor State: \"{}\"",
|
||||||
|
marked_text.escape_debug().to_string()
|
||||||
|
));
|
||||||
|
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
|
||||||
|
self.editor.update(self.cx, |editor, cx| {
|
||||||
|
editor.set_text(unmarked_text, cx);
|
||||||
|
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||||
|
s.select_ranges(selection_ranges)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
_state_context
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make an assertion about the editor's text and the ranges and directions
|
||||||
|
/// of its selections using a string containing embedded range markers.
|
||||||
|
///
|
||||||
|
/// See the `util::test::marked_text_ranges` function for more information.
|
||||||
|
pub fn assert_editor_state(&mut self, marked_text: &str) {
|
||||||
|
let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true);
|
||||||
|
let buffer_text = self.buffer_text();
|
||||||
|
assert_eq!(
|
||||||
|
buffer_text, unmarked_text,
|
||||||
|
"Unmarked text doesn't match buffer text"
|
||||||
|
);
|
||||||
|
self.assert_selections(expected_selections, marked_text.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
|
||||||
|
let expected_ranges = self.ranges(marked_text);
|
||||||
|
let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
|
||||||
|
let snapshot = editor.snapshot(cx);
|
||||||
|
editor
|
||||||
|
.background_highlights
|
||||||
|
.get(&TypeId::of::<Tag>())
|
||||||
|
.map(|h| h.1.clone())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
|
assert_set_eq!(actual_ranges, expected_ranges);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
|
||||||
|
let expected_ranges = self.ranges(marked_text);
|
||||||
|
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
|
||||||
|
let actual_ranges: Vec<Range<usize>> = snapshot
|
||||||
|
.highlight_ranges::<Tag>()
|
||||||
|
.map(|ranges| ranges.as_ref().clone().1)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
|
||||||
|
.collect();
|
||||||
|
assert_set_eq!(actual_ranges, expected_ranges);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
|
||||||
|
let expected_marked_text =
|
||||||
|
generate_marked_text(&self.buffer_text(), &expected_selections, true);
|
||||||
|
self.assert_selections(expected_selections, expected_marked_text)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_selections(
|
||||||
|
&mut self,
|
||||||
|
expected_selections: Vec<Range<usize>>,
|
||||||
|
expected_marked_text: String,
|
||||||
|
) {
|
||||||
|
let actual_selections = self
|
||||||
|
.editor
|
||||||
|
.read_with(self.cx, |editor, cx| editor.selections.all::<usize>(cx))
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| {
|
||||||
|
if s.reversed {
|
||||||
|
s.end..s.start
|
||||||
|
} else {
|
||||||
|
s.start..s.end
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let actual_marked_text =
|
||||||
|
generate_marked_text(&self.buffer_text(), &actual_selections, true);
|
||||||
|
if expected_selections != actual_selections {
|
||||||
|
panic!(
|
||||||
|
indoc! {"
|
||||||
|
{}Editor has unexpected selections.
|
||||||
|
|
||||||
|
Expected selections:
|
||||||
|
{}
|
||||||
|
|
||||||
|
Actual selections:
|
||||||
|
{}
|
||||||
|
"},
|
||||||
|
self.assertion_context(),
|
||||||
|
expected_marked_text,
|
||||||
|
actual_marked_text,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Deref for EditorTestContext<'a> {
|
||||||
|
type Target = gpui::TestAppContext;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.cx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> DerefMut for EditorTestContext<'a> {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.cx
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,6 +25,7 @@ env_logger = { version = "0.9", optional = true }
|
||||||
etagere = "0.2"
|
etagere = "0.2"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
image = "0.23"
|
image = "0.23"
|
||||||
|
itertools = "0.10"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||||
num_cpus = "1.13"
|
num_cpus = "1.13"
|
||||||
|
|
|
@ -1,28 +1,8 @@
|
||||||
pub mod action;
|
pub mod action;
|
||||||
mod callback_collection;
|
mod callback_collection;
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub mod test_app_context;
|
||||||
|
|
||||||
use crate::{
|
|
||||||
elements::ElementBox,
|
|
||||||
executor::{self, Task},
|
|
||||||
geometry::rect::RectF,
|
|
||||||
keymap::{self, Binding, Keystroke},
|
|
||||||
platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions},
|
|
||||||
presenter::Presenter,
|
|
||||||
util::post_inc,
|
|
||||||
Appearance, AssetCache, AssetSource, ClipboardItem, FontCache, InputHandler, MouseButton,
|
|
||||||
MouseRegionId, PathPromptOptions, TextLayoutCache,
|
|
||||||
};
|
|
||||||
pub use action::*;
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
|
||||||
use callback_collection::CallbackCollection;
|
|
||||||
use collections::{btree_map, hash_map::Entry, BTreeMap, HashMap, HashSet, VecDeque};
|
|
||||||
use keymap::MatchResult;
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
use platform::Event;
|
|
||||||
use postage::oneshot;
|
|
||||||
use smallvec::SmallVec;
|
|
||||||
use smol::prelude::*;
|
|
||||||
use std::{
|
use std::{
|
||||||
any::{type_name, Any, TypeId},
|
any::{type_name, Any, TypeId},
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
|
@ -38,7 +18,32 @@ use std::{
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::callback_collection::Mapping;
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use postage::oneshot;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use smol::prelude::*;
|
||||||
|
|
||||||
|
pub use action::*;
|
||||||
|
use callback_collection::{CallbackCollection, Mapping};
|
||||||
|
use collections::{btree_map, hash_map::Entry, BTreeMap, HashMap, HashSet, VecDeque};
|
||||||
|
use keymap::MatchResult;
|
||||||
|
use platform::Event;
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub use test_app_context::{ContextHandle, TestAppContext};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
elements::ElementBox,
|
||||||
|
executor::{self, Task},
|
||||||
|
geometry::rect::RectF,
|
||||||
|
keymap::{self, Binding, Keystroke},
|
||||||
|
platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions},
|
||||||
|
presenter::Presenter,
|
||||||
|
util::post_inc,
|
||||||
|
Appearance, AssetCache, AssetSource, ClipboardItem, FontCache, InputHandler, MouseButton,
|
||||||
|
MouseRegionId, PathPromptOptions, TextLayoutCache,
|
||||||
|
};
|
||||||
|
|
||||||
pub trait Entity: 'static {
|
pub trait Entity: 'static {
|
||||||
type Event;
|
type Event;
|
||||||
|
@ -177,13 +182,6 @@ pub struct App(Rc<RefCell<MutableAppContext>>);
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AsyncAppContext(Rc<RefCell<MutableAppContext>>);
|
pub struct AsyncAppContext(Rc<RefCell<MutableAppContext>>);
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
pub struct TestAppContext {
|
|
||||||
cx: Rc<RefCell<MutableAppContext>>,
|
|
||||||
foreground_platform: Rc<platform::test::ForegroundPlatform>,
|
|
||||||
condition_duration: Option<Duration>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct WindowInputHandler {
|
pub struct WindowInputHandler {
|
||||||
app: Rc<RefCell<MutableAppContext>>,
|
app: Rc<RefCell<MutableAppContext>>,
|
||||||
window_id: usize,
|
window_id: usize,
|
||||||
|
@ -427,327 +425,6 @@ impl InputHandler for WindowInputHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
impl TestAppContext {
|
|
||||||
pub fn new(
|
|
||||||
foreground_platform: Rc<platform::test::ForegroundPlatform>,
|
|
||||||
platform: Arc<dyn Platform>,
|
|
||||||
foreground: Rc<executor::Foreground>,
|
|
||||||
background: Arc<executor::Background>,
|
|
||||||
font_cache: Arc<FontCache>,
|
|
||||||
leak_detector: Arc<Mutex<LeakDetector>>,
|
|
||||||
first_entity_id: usize,
|
|
||||||
) -> Self {
|
|
||||||
let mut cx = MutableAppContext::new(
|
|
||||||
foreground,
|
|
||||||
background,
|
|
||||||
platform,
|
|
||||||
foreground_platform.clone(),
|
|
||||||
font_cache,
|
|
||||||
RefCounts {
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
leak_detector,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
(),
|
|
||||||
);
|
|
||||||
cx.next_entity_id = first_entity_id;
|
|
||||||
let cx = TestAppContext {
|
|
||||||
cx: Rc::new(RefCell::new(cx)),
|
|
||||||
foreground_platform,
|
|
||||||
condition_duration: None,
|
|
||||||
};
|
|
||||||
cx.cx.borrow_mut().weak_self = Some(Rc::downgrade(&cx.cx));
|
|
||||||
cx
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dispatch_action<A: Action>(&self, window_id: usize, action: A) {
|
|
||||||
let mut cx = self.cx.borrow_mut();
|
|
||||||
if let Some(view_id) = cx.focused_view_id(window_id) {
|
|
||||||
cx.handle_dispatch_action_from_effect(window_id, Some(view_id), &action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dispatch_global_action<A: Action>(&self, action: A) {
|
|
||||||
self.cx.borrow_mut().dispatch_global_action(action);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dispatch_keystroke(&mut self, window_id: usize, keystroke: Keystroke, is_held: bool) {
|
|
||||||
let handled = self.cx.borrow_mut().update(|cx| {
|
|
||||||
let presenter = cx
|
|
||||||
.presenters_and_platform_windows
|
|
||||||
.get(&window_id)
|
|
||||||
.unwrap()
|
|
||||||
.0
|
|
||||||
.clone();
|
|
||||||
|
|
||||||
if cx.dispatch_keystroke(window_id, &keystroke) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if presenter.borrow_mut().dispatch_event(
|
|
||||||
Event::KeyDown(KeyDownEvent {
|
|
||||||
keystroke: keystroke.clone(),
|
|
||||||
is_held,
|
|
||||||
}),
|
|
||||||
false,
|
|
||||||
cx,
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
});
|
|
||||||
|
|
||||||
if !handled && !keystroke.cmd && !keystroke.ctrl {
|
|
||||||
WindowInputHandler {
|
|
||||||
app: self.cx.clone(),
|
|
||||||
window_id,
|
|
||||||
}
|
|
||||||
.replace_text_in_range(None, &keystroke.key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
|
|
||||||
where
|
|
||||||
T: Entity,
|
|
||||||
F: FnOnce(&mut ModelContext<T>) -> T,
|
|
||||||
{
|
|
||||||
self.cx.borrow_mut().add_model(build_model)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_window<T, F>(&mut self, build_root_view: F) -> (usize, ViewHandle<T>)
|
|
||||||
where
|
|
||||||
T: View,
|
|
||||||
F: FnOnce(&mut ViewContext<T>) -> T,
|
|
||||||
{
|
|
||||||
let (window_id, view) = self
|
|
||||||
.cx
|
|
||||||
.borrow_mut()
|
|
||||||
.add_window(Default::default(), build_root_view);
|
|
||||||
self.simulate_window_activation(Some(window_id));
|
|
||||||
(window_id, view)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_view<T, F>(
|
|
||||||
&mut self,
|
|
||||||
parent_handle: impl Into<AnyViewHandle>,
|
|
||||||
build_view: F,
|
|
||||||
) -> ViewHandle<T>
|
|
||||||
where
|
|
||||||
T: View,
|
|
||||||
F: FnOnce(&mut ViewContext<T>) -> T,
|
|
||||||
{
|
|
||||||
self.cx.borrow_mut().add_view(parent_handle, build_view)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn window_ids(&self) -> Vec<usize> {
|
|
||||||
self.cx.borrow().window_ids().collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn root_view<T: View>(&self, window_id: usize) -> Option<ViewHandle<T>> {
|
|
||||||
self.cx.borrow().root_view(window_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read<T, F: FnOnce(&AppContext) -> T>(&self, callback: F) -> T {
|
|
||||||
callback(self.cx.borrow().as_ref())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update<T, F: FnOnce(&mut MutableAppContext) -> T>(&mut self, callback: F) -> T {
|
|
||||||
let mut state = self.cx.borrow_mut();
|
|
||||||
// Don't increment pending flushes in order for effects to be flushed before the callback
|
|
||||||
// completes, which is helpful in tests.
|
|
||||||
let result = callback(&mut *state);
|
|
||||||
// Flush effects after the callback just in case there are any. This can happen in edge
|
|
||||||
// cases such as the closure dropping handles.
|
|
||||||
state.flush_effects();
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render<F, V, T>(&mut self, handle: &ViewHandle<V>, f: F) -> T
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut V, &mut RenderContext<V>) -> T,
|
|
||||||
V: View,
|
|
||||||
{
|
|
||||||
handle.update(&mut *self.cx.borrow_mut(), |view, cx| {
|
|
||||||
let mut render_cx = RenderContext {
|
|
||||||
app: cx,
|
|
||||||
window_id: handle.window_id(),
|
|
||||||
view_id: handle.id(),
|
|
||||||
view_type: PhantomData,
|
|
||||||
titlebar_height: 0.,
|
|
||||||
hovered_region_ids: Default::default(),
|
|
||||||
clicked_region_ids: None,
|
|
||||||
refreshing: false,
|
|
||||||
appearance: Appearance::Light,
|
|
||||||
};
|
|
||||||
f(view, &mut render_cx)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_async(&self) -> AsyncAppContext {
|
|
||||||
AsyncAppContext(self.cx.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn font_cache(&self) -> Arc<FontCache> {
|
|
||||||
self.cx.borrow().cx.font_cache.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn foreground_platform(&self) -> Rc<platform::test::ForegroundPlatform> {
|
|
||||||
self.foreground_platform.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn platform(&self) -> Arc<dyn platform::Platform> {
|
|
||||||
self.cx.borrow().cx.platform.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn foreground(&self) -> Rc<executor::Foreground> {
|
|
||||||
self.cx.borrow().foreground().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn background(&self) -> Arc<executor::Background> {
|
|
||||||
self.cx.borrow().background().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn spawn<F, Fut, T>(&self, f: F) -> Task<T>
|
|
||||||
where
|
|
||||||
F: FnOnce(AsyncAppContext) -> Fut,
|
|
||||||
Fut: 'static + Future<Output = T>,
|
|
||||||
T: 'static,
|
|
||||||
{
|
|
||||||
let foreground = self.foreground();
|
|
||||||
let future = f(self.to_async());
|
|
||||||
let cx = self.to_async();
|
|
||||||
foreground.spawn(async move {
|
|
||||||
let result = future.await;
|
|
||||||
cx.0.borrow_mut().flush_effects();
|
|
||||||
result
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn simulate_new_path_selection(&self, result: impl FnOnce(PathBuf) -> Option<PathBuf>) {
|
|
||||||
self.foreground_platform.simulate_new_path_selection(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn did_prompt_for_new_path(&self) -> bool {
|
|
||||||
self.foreground_platform.as_ref().did_prompt_for_new_path()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn simulate_prompt_answer(&self, window_id: usize, answer: usize) {
|
|
||||||
use postage::prelude::Sink as _;
|
|
||||||
|
|
||||||
let mut done_tx = self
|
|
||||||
.window_mut(window_id)
|
|
||||||
.pending_prompts
|
|
||||||
.borrow_mut()
|
|
||||||
.pop_front()
|
|
||||||
.expect("prompt was not called");
|
|
||||||
let _ = done_tx.try_send(answer);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_pending_prompt(&self, window_id: usize) -> bool {
|
|
||||||
let window = self.window_mut(window_id);
|
|
||||||
let prompts = window.pending_prompts.borrow_mut();
|
|
||||||
!prompts.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn current_window_title(&self, window_id: usize) -> Option<String> {
|
|
||||||
self.window_mut(window_id).title.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn simulate_window_close(&self, window_id: usize) -> bool {
|
|
||||||
let handler = self.window_mut(window_id).should_close_handler.take();
|
|
||||||
if let Some(mut handler) = handler {
|
|
||||||
let should_close = handler();
|
|
||||||
self.window_mut(window_id).should_close_handler = Some(handler);
|
|
||||||
should_close
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn simulate_window_activation(&self, to_activate: Option<usize>) {
|
|
||||||
let mut handlers = BTreeMap::new();
|
|
||||||
{
|
|
||||||
let mut cx = self.cx.borrow_mut();
|
|
||||||
for (window_id, (_, window)) in &mut cx.presenters_and_platform_windows {
|
|
||||||
let window = window
|
|
||||||
.as_any_mut()
|
|
||||||
.downcast_mut::<platform::test::Window>()
|
|
||||||
.unwrap();
|
|
||||||
handlers.insert(
|
|
||||||
*window_id,
|
|
||||||
mem::take(&mut window.active_status_change_handlers),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let mut handlers = handlers.into_iter().collect::<Vec<_>>();
|
|
||||||
handlers.sort_unstable_by_key(|(window_id, _)| Some(*window_id) == to_activate);
|
|
||||||
|
|
||||||
for (window_id, mut window_handlers) in handlers {
|
|
||||||
for window_handler in &mut window_handlers {
|
|
||||||
window_handler(Some(window_id) == to_activate);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.window_mut(window_id)
|
|
||||||
.active_status_change_handlers
|
|
||||||
.extend(window_handlers);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_window_edited(&self, window_id: usize) -> bool {
|
|
||||||
self.window_mut(window_id).edited
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn leak_detector(&self) -> Arc<Mutex<LeakDetector>> {
|
|
||||||
self.cx.borrow().leak_detector()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn assert_dropped(&self, handle: impl WeakHandle) {
|
|
||||||
self.cx
|
|
||||||
.borrow()
|
|
||||||
.leak_detector()
|
|
||||||
.lock()
|
|
||||||
.assert_dropped(handle.id())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn window_mut(&self, window_id: usize) -> std::cell::RefMut<platform::test::Window> {
|
|
||||||
std::cell::RefMut::map(self.cx.borrow_mut(), |state| {
|
|
||||||
let (_, window) = state
|
|
||||||
.presenters_and_platform_windows
|
|
||||||
.get_mut(&window_id)
|
|
||||||
.unwrap();
|
|
||||||
let test_window = window
|
|
||||||
.as_any_mut()
|
|
||||||
.downcast_mut::<platform::test::Window>()
|
|
||||||
.unwrap();
|
|
||||||
test_window
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_condition_duration(&mut self, duration: Option<Duration>) {
|
|
||||||
self.condition_duration = duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn condition_duration(&self) -> Duration {
|
|
||||||
self.condition_duration.unwrap_or_else(|| {
|
|
||||||
if std::env::var("CI").is_ok() {
|
|
||||||
Duration::from_secs(2)
|
|
||||||
} else {
|
|
||||||
Duration::from_millis(500)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) {
|
|
||||||
self.update(|cx| {
|
|
||||||
let actual_content = cx.read_from_clipboard().map(|item| item.text().to_owned());
|
|
||||||
let expected_content = expected_content.map(|content| content.to_owned());
|
|
||||||
assert_eq!(actual_content, expected_content);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsyncAppContext {
|
impl AsyncAppContext {
|
||||||
pub fn spawn<F, Fut, T>(&self, f: F) -> Task<T>
|
pub fn spawn<F, Fut, T>(&self, f: F) -> Task<T>
|
||||||
where
|
where
|
||||||
|
@ -894,60 +571,6 @@ impl ReadViewWith for AsyncAppContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
impl UpdateModel for TestAppContext {
|
|
||||||
fn update_model<T: Entity, O>(
|
|
||||||
&mut self,
|
|
||||||
handle: &ModelHandle<T>,
|
|
||||||
update: &mut dyn FnMut(&mut T, &mut ModelContext<T>) -> O,
|
|
||||||
) -> O {
|
|
||||||
self.cx.borrow_mut().update_model(handle, update)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
impl ReadModelWith for TestAppContext {
|
|
||||||
fn read_model_with<E: Entity, T>(
|
|
||||||
&self,
|
|
||||||
handle: &ModelHandle<E>,
|
|
||||||
read: &mut dyn FnMut(&E, &AppContext) -> T,
|
|
||||||
) -> T {
|
|
||||||
let cx = self.cx.borrow();
|
|
||||||
let cx = cx.as_ref();
|
|
||||||
read(handle.read(cx), cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
impl UpdateView for TestAppContext {
|
|
||||||
fn update_view<T, S>(
|
|
||||||
&mut self,
|
|
||||||
handle: &ViewHandle<T>,
|
|
||||||
update: &mut dyn FnMut(&mut T, &mut ViewContext<T>) -> S,
|
|
||||||
) -> S
|
|
||||||
where
|
|
||||||
T: View,
|
|
||||||
{
|
|
||||||
self.cx.borrow_mut().update_view(handle, update)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
impl ReadViewWith for TestAppContext {
|
|
||||||
fn read_view_with<V, T>(
|
|
||||||
&self,
|
|
||||||
handle: &ViewHandle<V>,
|
|
||||||
read: &mut dyn FnMut(&V, &AppContext) -> T,
|
|
||||||
) -> T
|
|
||||||
where
|
|
||||||
V: View,
|
|
||||||
{
|
|
||||||
let cx = self.cx.borrow();
|
|
||||||
let cx = cx.as_ref();
|
|
||||||
read(handle.read(cx), cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ActionCallback =
|
type ActionCallback =
|
||||||
dyn FnMut(&mut dyn AnyView, &dyn Action, &mut MutableAppContext, usize, usize);
|
dyn FnMut(&mut dyn AnyView, &dyn Action, &mut MutableAppContext, usize, usize);
|
||||||
type GlobalActionCallback = dyn FnMut(&dyn Action, &mut MutableAppContext);
|
type GlobalActionCallback = dyn FnMut(&dyn Action, &mut MutableAppContext);
|
||||||
|
@ -4446,117 +4069,6 @@ impl<T: Entity> ModelHandle<T> {
|
||||||
update(model, cx)
|
update(model, cx)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
|
|
||||||
let (tx, mut rx) = futures::channel::mpsc::unbounded();
|
|
||||||
let mut cx = cx.cx.borrow_mut();
|
|
||||||
let subscription = cx.observe(self, move |_, _| {
|
|
||||||
tx.unbounded_send(()).ok();
|
|
||||||
});
|
|
||||||
|
|
||||||
let duration = if std::env::var("CI").is_ok() {
|
|
||||||
Duration::from_secs(5)
|
|
||||||
} else {
|
|
||||||
Duration::from_secs(1)
|
|
||||||
};
|
|
||||||
|
|
||||||
async move {
|
|
||||||
let notification = crate::util::timeout(duration, rx.next())
|
|
||||||
.await
|
|
||||||
.expect("next notification timed out");
|
|
||||||
drop(subscription);
|
|
||||||
notification.expect("model dropped while test was waiting for its next notification")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
pub fn next_event(&self, cx: &TestAppContext) -> impl Future<Output = T::Event>
|
|
||||||
where
|
|
||||||
T::Event: Clone,
|
|
||||||
{
|
|
||||||
let (tx, mut rx) = futures::channel::mpsc::unbounded();
|
|
||||||
let mut cx = cx.cx.borrow_mut();
|
|
||||||
let subscription = cx.subscribe(self, move |_, event, _| {
|
|
||||||
tx.unbounded_send(event.clone()).ok();
|
|
||||||
});
|
|
||||||
|
|
||||||
let duration = if std::env::var("CI").is_ok() {
|
|
||||||
Duration::from_secs(5)
|
|
||||||
} else {
|
|
||||||
Duration::from_secs(1)
|
|
||||||
};
|
|
||||||
|
|
||||||
cx.foreground.start_waiting();
|
|
||||||
async move {
|
|
||||||
let event = crate::util::timeout(duration, rx.next())
|
|
||||||
.await
|
|
||||||
.expect("next event timed out");
|
|
||||||
drop(subscription);
|
|
||||||
event.expect("model dropped while test was waiting for its next event")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
pub fn condition(
|
|
||||||
&self,
|
|
||||||
cx: &TestAppContext,
|
|
||||||
mut predicate: impl FnMut(&T, &AppContext) -> bool,
|
|
||||||
) -> impl Future<Output = ()> {
|
|
||||||
let (tx, mut rx) = futures::channel::mpsc::unbounded();
|
|
||||||
|
|
||||||
let mut cx = cx.cx.borrow_mut();
|
|
||||||
let subscriptions = (
|
|
||||||
cx.observe(self, {
|
|
||||||
let tx = tx.clone();
|
|
||||||
move |_, _| {
|
|
||||||
tx.unbounded_send(()).ok();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
cx.subscribe(self, {
|
|
||||||
move |_, _, _| {
|
|
||||||
tx.unbounded_send(()).ok();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap();
|
|
||||||
let handle = self.downgrade();
|
|
||||||
let duration = if std::env::var("CI").is_ok() {
|
|
||||||
Duration::from_secs(5)
|
|
||||||
} else {
|
|
||||||
Duration::from_secs(1)
|
|
||||||
};
|
|
||||||
|
|
||||||
async move {
|
|
||||||
crate::util::timeout(duration, async move {
|
|
||||||
loop {
|
|
||||||
{
|
|
||||||
let cx = cx.borrow();
|
|
||||||
let cx = cx.as_ref();
|
|
||||||
if predicate(
|
|
||||||
handle
|
|
||||||
.upgrade(cx)
|
|
||||||
.expect("model dropped with pending condition")
|
|
||||||
.read(cx),
|
|
||||||
cx,
|
|
||||||
) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.borrow().foreground().start_waiting();
|
|
||||||
rx.next()
|
|
||||||
.await
|
|
||||||
.expect("model dropped with pending condition");
|
|
||||||
cx.borrow().foreground().finish_waiting();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.expect("condition timed out");
|
|
||||||
drop(subscriptions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Entity> Clone for ModelHandle<T> {
|
impl<T: Entity> Clone for ModelHandle<T> {
|
||||||
|
@ -4789,93 +4301,6 @@ impl<T: View> ViewHandle<T> {
|
||||||
cx.focused_view_id(self.window_id)
|
cx.focused_view_id(self.window_id)
|
||||||
.map_or(false, |focused_id| focused_id == self.view_id)
|
.map_or(false, |focused_id| focused_id == self.view_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
|
|
||||||
use postage::prelude::{Sink as _, Stream as _};
|
|
||||||
|
|
||||||
let (mut tx, mut rx) = postage::mpsc::channel(1);
|
|
||||||
let mut cx = cx.cx.borrow_mut();
|
|
||||||
let subscription = cx.observe(self, move |_, _| {
|
|
||||||
tx.try_send(()).ok();
|
|
||||||
});
|
|
||||||
|
|
||||||
let duration = if std::env::var("CI").is_ok() {
|
|
||||||
Duration::from_secs(5)
|
|
||||||
} else {
|
|
||||||
Duration::from_secs(1)
|
|
||||||
};
|
|
||||||
|
|
||||||
async move {
|
|
||||||
let notification = crate::util::timeout(duration, rx.recv())
|
|
||||||
.await
|
|
||||||
.expect("next notification timed out");
|
|
||||||
drop(subscription);
|
|
||||||
notification.expect("model dropped while test was waiting for its next notification")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
pub fn condition(
|
|
||||||
&self,
|
|
||||||
cx: &TestAppContext,
|
|
||||||
mut predicate: impl FnMut(&T, &AppContext) -> bool,
|
|
||||||
) -> impl Future<Output = ()> {
|
|
||||||
use postage::prelude::{Sink as _, Stream as _};
|
|
||||||
|
|
||||||
let (tx, mut rx) = postage::mpsc::channel(1024);
|
|
||||||
let timeout_duration = cx.condition_duration();
|
|
||||||
|
|
||||||
let mut cx = cx.cx.borrow_mut();
|
|
||||||
let subscriptions = self.update(&mut *cx, |_, cx| {
|
|
||||||
(
|
|
||||||
cx.observe(self, {
|
|
||||||
let mut tx = tx.clone();
|
|
||||||
move |_, _, _| {
|
|
||||||
tx.blocking_send(()).ok();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
cx.subscribe(self, {
|
|
||||||
let mut tx = tx.clone();
|
|
||||||
move |_, _, _, _| {
|
|
||||||
tx.blocking_send(()).ok();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap();
|
|
||||||
let handle = self.downgrade();
|
|
||||||
|
|
||||||
async move {
|
|
||||||
crate::util::timeout(timeout_duration, async move {
|
|
||||||
loop {
|
|
||||||
{
|
|
||||||
let cx = cx.borrow();
|
|
||||||
let cx = cx.as_ref();
|
|
||||||
if predicate(
|
|
||||||
handle
|
|
||||||
.upgrade(cx)
|
|
||||||
.expect("view dropped with pending condition")
|
|
||||||
.read(cx),
|
|
||||||
cx,
|
|
||||||
) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.borrow().foreground().start_waiting();
|
|
||||||
rx.recv()
|
|
||||||
.await
|
|
||||||
.expect("view dropped with pending condition");
|
|
||||||
cx.borrow().foreground().finish_waiting();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.expect("condition timed out");
|
|
||||||
drop(subscriptions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: View> Clone for ViewHandle<T> {
|
impl<T: View> Clone for ViewHandle<T> {
|
||||||
|
|
655
crates/gpui/src/app/test_app_context.rs
Normal file
655
crates/gpui/src/app/test_app_context.rs
Normal file
|
@ -0,0 +1,655 @@
|
||||||
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
marker::PhantomData,
|
||||||
|
mem,
|
||||||
|
path::PathBuf,
|
||||||
|
rc::Rc,
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicUsize, Ordering},
|
||||||
|
Arc,
|
||||||
|
},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use futures::Future;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use parking_lot::{Mutex, RwLock};
|
||||||
|
use smol::stream::StreamExt;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
executor, keymap::Keystroke, platform, Action, AnyViewHandle, AppContext, Appearance, Entity,
|
||||||
|
Event, FontCache, InputHandler, KeyDownEvent, LeakDetector, ModelContext, ModelHandle,
|
||||||
|
MutableAppContext, Platform, ReadModelWith, ReadViewWith, RenderContext, Task, UpdateModel,
|
||||||
|
UpdateView, View, ViewContext, ViewHandle, WeakHandle, WindowInputHandler,
|
||||||
|
};
|
||||||
|
use collections::BTreeMap;
|
||||||
|
|
||||||
|
use super::{AsyncAppContext, RefCounts};
|
||||||
|
|
||||||
|
pub struct TestAppContext {
|
||||||
|
cx: Rc<RefCell<MutableAppContext>>,
|
||||||
|
foreground_platform: Rc<platform::test::ForegroundPlatform>,
|
||||||
|
condition_duration: Option<Duration>,
|
||||||
|
pub function_name: String,
|
||||||
|
assertion_context: AssertionContextManager,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestAppContext {
|
||||||
|
pub fn new(
|
||||||
|
foreground_platform: Rc<platform::test::ForegroundPlatform>,
|
||||||
|
platform: Arc<dyn Platform>,
|
||||||
|
foreground: Rc<executor::Foreground>,
|
||||||
|
background: Arc<executor::Background>,
|
||||||
|
font_cache: Arc<FontCache>,
|
||||||
|
leak_detector: Arc<Mutex<LeakDetector>>,
|
||||||
|
first_entity_id: usize,
|
||||||
|
function_name: String,
|
||||||
|
) -> Self {
|
||||||
|
let mut cx = MutableAppContext::new(
|
||||||
|
foreground,
|
||||||
|
background,
|
||||||
|
platform,
|
||||||
|
foreground_platform.clone(),
|
||||||
|
font_cache,
|
||||||
|
RefCounts {
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
leak_detector,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
(),
|
||||||
|
);
|
||||||
|
cx.next_entity_id = first_entity_id;
|
||||||
|
let cx = TestAppContext {
|
||||||
|
cx: Rc::new(RefCell::new(cx)),
|
||||||
|
foreground_platform,
|
||||||
|
condition_duration: None,
|
||||||
|
function_name,
|
||||||
|
assertion_context: AssertionContextManager::new(),
|
||||||
|
};
|
||||||
|
cx.cx.borrow_mut().weak_self = Some(Rc::downgrade(&cx.cx));
|
||||||
|
cx
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dispatch_action<A: Action>(&self, window_id: usize, action: A) {
|
||||||
|
let mut cx = self.cx.borrow_mut();
|
||||||
|
if let Some(view_id) = cx.focused_view_id(window_id) {
|
||||||
|
cx.handle_dispatch_action_from_effect(window_id, Some(view_id), &action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dispatch_global_action<A: Action>(&self, action: A) {
|
||||||
|
self.cx.borrow_mut().dispatch_global_action(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dispatch_keystroke(&mut self, window_id: usize, keystroke: Keystroke, is_held: bool) {
|
||||||
|
let handled = self.cx.borrow_mut().update(|cx| {
|
||||||
|
let presenter = cx
|
||||||
|
.presenters_and_platform_windows
|
||||||
|
.get(&window_id)
|
||||||
|
.unwrap()
|
||||||
|
.0
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
if cx.dispatch_keystroke(window_id, &keystroke) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if presenter.borrow_mut().dispatch_event(
|
||||||
|
Event::KeyDown(KeyDownEvent {
|
||||||
|
keystroke: keystroke.clone(),
|
||||||
|
is_held,
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
cx,
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
});
|
||||||
|
|
||||||
|
if !handled && !keystroke.cmd && !keystroke.ctrl {
|
||||||
|
WindowInputHandler {
|
||||||
|
app: self.cx.clone(),
|
||||||
|
window_id,
|
||||||
|
}
|
||||||
|
.replace_text_in_range(None, &keystroke.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
|
||||||
|
where
|
||||||
|
T: Entity,
|
||||||
|
F: FnOnce(&mut ModelContext<T>) -> T,
|
||||||
|
{
|
||||||
|
self.cx.borrow_mut().add_model(build_model)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_window<T, F>(&mut self, build_root_view: F) -> (usize, ViewHandle<T>)
|
||||||
|
where
|
||||||
|
T: View,
|
||||||
|
F: FnOnce(&mut ViewContext<T>) -> T,
|
||||||
|
{
|
||||||
|
let (window_id, view) = self
|
||||||
|
.cx
|
||||||
|
.borrow_mut()
|
||||||
|
.add_window(Default::default(), build_root_view);
|
||||||
|
self.simulate_window_activation(Some(window_id));
|
||||||
|
(window_id, view)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_view<T, F>(
|
||||||
|
&mut self,
|
||||||
|
parent_handle: impl Into<AnyViewHandle>,
|
||||||
|
build_view: F,
|
||||||
|
) -> ViewHandle<T>
|
||||||
|
where
|
||||||
|
T: View,
|
||||||
|
F: FnOnce(&mut ViewContext<T>) -> T,
|
||||||
|
{
|
||||||
|
self.cx.borrow_mut().add_view(parent_handle, build_view)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn window_ids(&self) -> Vec<usize> {
|
||||||
|
self.cx.borrow().window_ids().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn root_view<T: View>(&self, window_id: usize) -> Option<ViewHandle<T>> {
|
||||||
|
self.cx.borrow().root_view(window_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read<T, F: FnOnce(&AppContext) -> T>(&self, callback: F) -> T {
|
||||||
|
callback(self.cx.borrow().as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update<T, F: FnOnce(&mut MutableAppContext) -> T>(&mut self, callback: F) -> T {
|
||||||
|
let mut state = self.cx.borrow_mut();
|
||||||
|
// Don't increment pending flushes in order for effects to be flushed before the callback
|
||||||
|
// completes, which is helpful in tests.
|
||||||
|
let result = callback(&mut *state);
|
||||||
|
// Flush effects after the callback just in case there are any. This can happen in edge
|
||||||
|
// cases such as the closure dropping handles.
|
||||||
|
state.flush_effects();
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render<F, V, T>(&mut self, handle: &ViewHandle<V>, f: F) -> T
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut V, &mut RenderContext<V>) -> T,
|
||||||
|
V: View,
|
||||||
|
{
|
||||||
|
handle.update(&mut *self.cx.borrow_mut(), |view, cx| {
|
||||||
|
let mut render_cx = RenderContext {
|
||||||
|
app: cx,
|
||||||
|
window_id: handle.window_id(),
|
||||||
|
view_id: handle.id(),
|
||||||
|
view_type: PhantomData,
|
||||||
|
titlebar_height: 0.,
|
||||||
|
hovered_region_ids: Default::default(),
|
||||||
|
clicked_region_ids: None,
|
||||||
|
refreshing: false,
|
||||||
|
appearance: Appearance::Light,
|
||||||
|
};
|
||||||
|
f(view, &mut render_cx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_async(&self) -> AsyncAppContext {
|
||||||
|
AsyncAppContext(self.cx.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn font_cache(&self) -> Arc<FontCache> {
|
||||||
|
self.cx.borrow().cx.font_cache.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn foreground_platform(&self) -> Rc<platform::test::ForegroundPlatform> {
|
||||||
|
self.foreground_platform.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn platform(&self) -> Arc<dyn platform::Platform> {
|
||||||
|
self.cx.borrow().cx.platform.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn foreground(&self) -> Rc<executor::Foreground> {
|
||||||
|
self.cx.borrow().foreground().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn background(&self) -> Arc<executor::Background> {
|
||||||
|
self.cx.borrow().background().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawn<F, Fut, T>(&self, f: F) -> Task<T>
|
||||||
|
where
|
||||||
|
F: FnOnce(AsyncAppContext) -> Fut,
|
||||||
|
Fut: 'static + Future<Output = T>,
|
||||||
|
T: 'static,
|
||||||
|
{
|
||||||
|
let foreground = self.foreground();
|
||||||
|
let future = f(self.to_async());
|
||||||
|
let cx = self.to_async();
|
||||||
|
foreground.spawn(async move {
|
||||||
|
let result = future.await;
|
||||||
|
cx.0.borrow_mut().flush_effects();
|
||||||
|
result
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn simulate_new_path_selection(&self, result: impl FnOnce(PathBuf) -> Option<PathBuf>) {
|
||||||
|
self.foreground_platform.simulate_new_path_selection(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn did_prompt_for_new_path(&self) -> bool {
|
||||||
|
self.foreground_platform.as_ref().did_prompt_for_new_path()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn simulate_prompt_answer(&self, window_id: usize, answer: usize) {
|
||||||
|
use postage::prelude::Sink as _;
|
||||||
|
|
||||||
|
let mut done_tx = self
|
||||||
|
.window_mut(window_id)
|
||||||
|
.pending_prompts
|
||||||
|
.borrow_mut()
|
||||||
|
.pop_front()
|
||||||
|
.expect("prompt was not called");
|
||||||
|
let _ = done_tx.try_send(answer);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_pending_prompt(&self, window_id: usize) -> bool {
|
||||||
|
let window = self.window_mut(window_id);
|
||||||
|
let prompts = window.pending_prompts.borrow_mut();
|
||||||
|
!prompts.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_window_title(&self, window_id: usize) -> Option<String> {
|
||||||
|
self.window_mut(window_id).title.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn simulate_window_close(&self, window_id: usize) -> bool {
|
||||||
|
let handler = self.window_mut(window_id).should_close_handler.take();
|
||||||
|
if let Some(mut handler) = handler {
|
||||||
|
let should_close = handler();
|
||||||
|
self.window_mut(window_id).should_close_handler = Some(handler);
|
||||||
|
should_close
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn simulate_window_activation(&self, to_activate: Option<usize>) {
|
||||||
|
let mut handlers = BTreeMap::new();
|
||||||
|
{
|
||||||
|
let mut cx = self.cx.borrow_mut();
|
||||||
|
for (window_id, (_, window)) in &mut cx.presenters_and_platform_windows {
|
||||||
|
let window = window
|
||||||
|
.as_any_mut()
|
||||||
|
.downcast_mut::<platform::test::Window>()
|
||||||
|
.unwrap();
|
||||||
|
handlers.insert(
|
||||||
|
*window_id,
|
||||||
|
mem::take(&mut window.active_status_change_handlers),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut handlers = handlers.into_iter().collect::<Vec<_>>();
|
||||||
|
handlers.sort_unstable_by_key(|(window_id, _)| Some(*window_id) == to_activate);
|
||||||
|
|
||||||
|
for (window_id, mut window_handlers) in handlers {
|
||||||
|
for window_handler in &mut window_handlers {
|
||||||
|
window_handler(Some(window_id) == to_activate);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.window_mut(window_id)
|
||||||
|
.active_status_change_handlers
|
||||||
|
.extend(window_handlers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_window_edited(&self, window_id: usize) -> bool {
|
||||||
|
self.window_mut(window_id).edited
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn leak_detector(&self) -> Arc<Mutex<LeakDetector>> {
|
||||||
|
self.cx.borrow().leak_detector()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assert_dropped(&self, handle: impl WeakHandle) {
|
||||||
|
self.cx
|
||||||
|
.borrow()
|
||||||
|
.leak_detector()
|
||||||
|
.lock()
|
||||||
|
.assert_dropped(handle.id())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_mut(&self, window_id: usize) -> std::cell::RefMut<platform::test::Window> {
|
||||||
|
std::cell::RefMut::map(self.cx.borrow_mut(), |state| {
|
||||||
|
let (_, window) = state
|
||||||
|
.presenters_and_platform_windows
|
||||||
|
.get_mut(&window_id)
|
||||||
|
.unwrap();
|
||||||
|
let test_window = window
|
||||||
|
.as_any_mut()
|
||||||
|
.downcast_mut::<platform::test::Window>()
|
||||||
|
.unwrap();
|
||||||
|
test_window
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_condition_duration(&mut self, duration: Option<Duration>) {
|
||||||
|
self.condition_duration = duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn condition_duration(&self) -> Duration {
|
||||||
|
self.condition_duration.unwrap_or_else(|| {
|
||||||
|
if std::env::var("CI").is_ok() {
|
||||||
|
Duration::from_secs(2)
|
||||||
|
} else {
|
||||||
|
Duration::from_millis(500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assert_clipboard_content(&mut self, expected_content: Option<&str>) {
|
||||||
|
self.update(|cx| {
|
||||||
|
let actual_content = cx.read_from_clipboard().map(|item| item.text().to_owned());
|
||||||
|
let expected_content = expected_content.map(|content| content.to_owned());
|
||||||
|
assert_eq!(actual_content, expected_content);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_assertion_context(&self, context: String) -> ContextHandle {
|
||||||
|
self.assertion_context.add_context(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assertion_context(&self) -> String {
|
||||||
|
self.assertion_context.context()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpdateModel for TestAppContext {
|
||||||
|
fn update_model<T: Entity, O>(
|
||||||
|
&mut self,
|
||||||
|
handle: &ModelHandle<T>,
|
||||||
|
update: &mut dyn FnMut(&mut T, &mut ModelContext<T>) -> O,
|
||||||
|
) -> O {
|
||||||
|
self.cx.borrow_mut().update_model(handle, update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReadModelWith for TestAppContext {
|
||||||
|
fn read_model_with<E: Entity, T>(
|
||||||
|
&self,
|
||||||
|
handle: &ModelHandle<E>,
|
||||||
|
read: &mut dyn FnMut(&E, &AppContext) -> T,
|
||||||
|
) -> T {
|
||||||
|
let cx = self.cx.borrow();
|
||||||
|
let cx = cx.as_ref();
|
||||||
|
read(handle.read(cx), cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpdateView for TestAppContext {
|
||||||
|
fn update_view<T, S>(
|
||||||
|
&mut self,
|
||||||
|
handle: &ViewHandle<T>,
|
||||||
|
update: &mut dyn FnMut(&mut T, &mut ViewContext<T>) -> S,
|
||||||
|
) -> S
|
||||||
|
where
|
||||||
|
T: View,
|
||||||
|
{
|
||||||
|
self.cx.borrow_mut().update_view(handle, update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReadViewWith for TestAppContext {
|
||||||
|
fn read_view_with<V, T>(
|
||||||
|
&self,
|
||||||
|
handle: &ViewHandle<V>,
|
||||||
|
read: &mut dyn FnMut(&V, &AppContext) -> T,
|
||||||
|
) -> T
|
||||||
|
where
|
||||||
|
V: View,
|
||||||
|
{
|
||||||
|
let cx = self.cx.borrow();
|
||||||
|
let cx = cx.as_ref();
|
||||||
|
read(handle.read(cx), cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Entity> ModelHandle<T> {
|
||||||
|
pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
|
||||||
|
let (tx, mut rx) = futures::channel::mpsc::unbounded();
|
||||||
|
let mut cx = cx.cx.borrow_mut();
|
||||||
|
let subscription = cx.observe(self, move |_, _| {
|
||||||
|
tx.unbounded_send(()).ok();
|
||||||
|
});
|
||||||
|
|
||||||
|
let duration = if std::env::var("CI").is_ok() {
|
||||||
|
Duration::from_secs(5)
|
||||||
|
} else {
|
||||||
|
Duration::from_secs(1)
|
||||||
|
};
|
||||||
|
|
||||||
|
async move {
|
||||||
|
let notification = crate::util::timeout(duration, rx.next())
|
||||||
|
.await
|
||||||
|
.expect("next notification timed out");
|
||||||
|
drop(subscription);
|
||||||
|
notification.expect("model dropped while test was waiting for its next notification")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_event(&self, cx: &TestAppContext) -> impl Future<Output = T::Event>
|
||||||
|
where
|
||||||
|
T::Event: Clone,
|
||||||
|
{
|
||||||
|
let (tx, mut rx) = futures::channel::mpsc::unbounded();
|
||||||
|
let mut cx = cx.cx.borrow_mut();
|
||||||
|
let subscription = cx.subscribe(self, move |_, event, _| {
|
||||||
|
tx.unbounded_send(event.clone()).ok();
|
||||||
|
});
|
||||||
|
|
||||||
|
let duration = if std::env::var("CI").is_ok() {
|
||||||
|
Duration::from_secs(5)
|
||||||
|
} else {
|
||||||
|
Duration::from_secs(1)
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.foreground.start_waiting();
|
||||||
|
async move {
|
||||||
|
let event = crate::util::timeout(duration, rx.next())
|
||||||
|
.await
|
||||||
|
.expect("next event timed out");
|
||||||
|
drop(subscription);
|
||||||
|
event.expect("model dropped while test was waiting for its next event")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn condition(
|
||||||
|
&self,
|
||||||
|
cx: &TestAppContext,
|
||||||
|
mut predicate: impl FnMut(&T, &AppContext) -> bool,
|
||||||
|
) -> impl Future<Output = ()> {
|
||||||
|
let (tx, mut rx) = futures::channel::mpsc::unbounded();
|
||||||
|
|
||||||
|
let mut cx = cx.cx.borrow_mut();
|
||||||
|
let subscriptions = (
|
||||||
|
cx.observe(self, {
|
||||||
|
let tx = tx.clone();
|
||||||
|
move |_, _| {
|
||||||
|
tx.unbounded_send(()).ok();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
cx.subscribe(self, {
|
||||||
|
move |_, _, _| {
|
||||||
|
tx.unbounded_send(()).ok();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap();
|
||||||
|
let handle = self.downgrade();
|
||||||
|
let duration = if std::env::var("CI").is_ok() {
|
||||||
|
Duration::from_secs(5)
|
||||||
|
} else {
|
||||||
|
Duration::from_secs(1)
|
||||||
|
};
|
||||||
|
|
||||||
|
async move {
|
||||||
|
crate::util::timeout(duration, async move {
|
||||||
|
loop {
|
||||||
|
{
|
||||||
|
let cx = cx.borrow();
|
||||||
|
let cx = cx.as_ref();
|
||||||
|
if predicate(
|
||||||
|
handle
|
||||||
|
.upgrade(cx)
|
||||||
|
.expect("model dropped with pending condition")
|
||||||
|
.read(cx),
|
||||||
|
cx,
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.borrow().foreground().start_waiting();
|
||||||
|
rx.next()
|
||||||
|
.await
|
||||||
|
.expect("model dropped with pending condition");
|
||||||
|
cx.borrow().foreground().finish_waiting();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("condition timed out");
|
||||||
|
drop(subscriptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: View> ViewHandle<T> {
|
||||||
|
pub fn next_notification(&self, cx: &TestAppContext) -> impl Future<Output = ()> {
|
||||||
|
use postage::prelude::{Sink as _, Stream as _};
|
||||||
|
|
||||||
|
let (mut tx, mut rx) = postage::mpsc::channel(1);
|
||||||
|
let mut cx = cx.cx.borrow_mut();
|
||||||
|
let subscription = cx.observe(self, move |_, _| {
|
||||||
|
tx.try_send(()).ok();
|
||||||
|
});
|
||||||
|
|
||||||
|
let duration = if std::env::var("CI").is_ok() {
|
||||||
|
Duration::from_secs(5)
|
||||||
|
} else {
|
||||||
|
Duration::from_secs(1)
|
||||||
|
};
|
||||||
|
|
||||||
|
async move {
|
||||||
|
let notification = crate::util::timeout(duration, rx.recv())
|
||||||
|
.await
|
||||||
|
.expect("next notification timed out");
|
||||||
|
drop(subscription);
|
||||||
|
notification.expect("model dropped while test was waiting for its next notification")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn condition(
|
||||||
|
&self,
|
||||||
|
cx: &TestAppContext,
|
||||||
|
mut predicate: impl FnMut(&T, &AppContext) -> bool,
|
||||||
|
) -> impl Future<Output = ()> {
|
||||||
|
use postage::prelude::{Sink as _, Stream as _};
|
||||||
|
|
||||||
|
let (tx, mut rx) = postage::mpsc::channel(1024);
|
||||||
|
let timeout_duration = cx.condition_duration();
|
||||||
|
|
||||||
|
let mut cx = cx.cx.borrow_mut();
|
||||||
|
let subscriptions = self.update(&mut *cx, |_, cx| {
|
||||||
|
(
|
||||||
|
cx.observe(self, {
|
||||||
|
let mut tx = tx.clone();
|
||||||
|
move |_, _, _| {
|
||||||
|
tx.blocking_send(()).ok();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
cx.subscribe(self, {
|
||||||
|
let mut tx = tx.clone();
|
||||||
|
move |_, _, _, _| {
|
||||||
|
tx.blocking_send(()).ok();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let cx = cx.weak_self.as_ref().unwrap().upgrade().unwrap();
|
||||||
|
let handle = self.downgrade();
|
||||||
|
|
||||||
|
async move {
|
||||||
|
crate::util::timeout(timeout_duration, async move {
|
||||||
|
loop {
|
||||||
|
{
|
||||||
|
let cx = cx.borrow();
|
||||||
|
let cx = cx.as_ref();
|
||||||
|
if predicate(
|
||||||
|
handle
|
||||||
|
.upgrade(cx)
|
||||||
|
.expect("view dropped with pending condition")
|
||||||
|
.read(cx),
|
||||||
|
cx,
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.borrow().foreground().start_waiting();
|
||||||
|
rx.recv()
|
||||||
|
.await
|
||||||
|
.expect("view dropped with pending condition");
|
||||||
|
cx.borrow().foreground().finish_waiting();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("condition timed out");
|
||||||
|
drop(subscriptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AssertionContextManager {
|
||||||
|
id: Arc<AtomicUsize>,
|
||||||
|
contexts: Arc<RwLock<BTreeMap<usize, String>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AssertionContextManager {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
id: Arc::new(AtomicUsize::new(0)),
|
||||||
|
contexts: Arc::new(RwLock::new(BTreeMap::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_context(&self, context: String) -> ContextHandle {
|
||||||
|
let id = self.id.fetch_add(1, Ordering::Relaxed);
|
||||||
|
let mut contexts = self.contexts.write();
|
||||||
|
contexts.insert(id, context);
|
||||||
|
ContextHandle {
|
||||||
|
id,
|
||||||
|
manager: self.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn context(&self) -> String {
|
||||||
|
let contexts = self.contexts.read();
|
||||||
|
format!("\n{}\n", contexts.values().join("\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ContextHandle {
|
||||||
|
id: usize,
|
||||||
|
manager: AssertionContextManager,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for ContextHandle {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let mut contexts = self.manager.contexts.write();
|
||||||
|
contexts.remove(&self.id);
|
||||||
|
}
|
||||||
|
}
|
|
@ -65,6 +65,7 @@ pub trait Platform: Send + Sync {
|
||||||
fn delete_credentials(&self, url: &str) -> Result<()>;
|
fn delete_credentials(&self, url: &str) -> Result<()>;
|
||||||
|
|
||||||
fn set_cursor_style(&self, style: CursorStyle);
|
fn set_cursor_style(&self, style: CursorStyle);
|
||||||
|
fn should_auto_hide_scrollbars(&self) -> bool;
|
||||||
|
|
||||||
fn local_timezone(&self) -> UtcOffset;
|
fn local_timezone(&self) -> UtcOffset;
|
||||||
|
|
||||||
|
|
|
@ -709,6 +709,16 @@ impl platform::Platform for MacPlatform {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn should_auto_hide_scrollbars(&self) -> bool {
|
||||||
|
#[allow(non_upper_case_globals)]
|
||||||
|
const NSScrollerStyleOverlay: NSInteger = 1;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let style: NSInteger = msg_send![class!(NSScroller), preferredScrollerStyle];
|
||||||
|
style == NSScrollerStyleOverlay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn local_timezone(&self) -> UtcOffset {
|
fn local_timezone(&self) -> UtcOffset {
|
||||||
unsafe {
|
unsafe {
|
||||||
let local_timezone: id = msg_send![class!(NSTimeZone), localTimeZone];
|
let local_timezone: id = msg_send![class!(NSTimeZone), localTimeZone];
|
||||||
|
|
|
@ -181,6 +181,10 @@ impl super::Platform for Platform {
|
||||||
*self.cursor.lock() = style;
|
*self.cursor.lock() = style;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn should_auto_hide_scrollbars(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
fn local_timezone(&self) -> UtcOffset {
|
fn local_timezone(&self) -> UtcOffset {
|
||||||
UtcOffset::UTC
|
UtcOffset::UTC
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,7 @@ pub fn run_test(
|
||||||
u64,
|
u64,
|
||||||
bool,
|
bool,
|
||||||
)),
|
)),
|
||||||
|
fn_name: String,
|
||||||
) {
|
) {
|
||||||
// let _profiler = dhat::Profiler::new_heap();
|
// let _profiler = dhat::Profiler::new_heap();
|
||||||
|
|
||||||
|
@ -78,6 +79,7 @@ pub fn run_test(
|
||||||
font_cache.clone(),
|
font_cache.clone(),
|
||||||
leak_detector.clone(),
|
leak_detector.clone(),
|
||||||
0,
|
0,
|
||||||
|
fn_name.clone(),
|
||||||
);
|
);
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
test_fn(
|
test_fn(
|
||||||
|
|
|
@ -117,6 +117,7 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
|
||||||
cx.font_cache().clone(),
|
cx.font_cache().clone(),
|
||||||
cx.leak_detector(),
|
cx.leak_detector(),
|
||||||
#first_entity_id,
|
#first_entity_id,
|
||||||
|
stringify!(#outer_fn_name).to_string(),
|
||||||
);
|
);
|
||||||
));
|
));
|
||||||
cx_teardowns.extend(quote!(
|
cx_teardowns.extend(quote!(
|
||||||
|
@ -149,7 +150,8 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
|
||||||
#cx_vars
|
#cx_vars
|
||||||
cx.foreground().run(#inner_fn_name(#inner_fn_args));
|
cx.foreground().run(#inner_fn_name(#inner_fn_args));
|
||||||
#cx_teardowns
|
#cx_teardowns
|
||||||
}
|
},
|
||||||
|
stringify!(#outer_fn_name).to_string(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -187,7 +189,8 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
|
||||||
#num_iterations as u64,
|
#num_iterations as u64,
|
||||||
#starting_seed as u64,
|
#starting_seed as u64,
|
||||||
#max_retries,
|
#max_retries,
|
||||||
&mut |cx, _, _, seed, is_last_iteration| #inner_fn_name(#inner_fn_args)
|
&mut |cx, _, _, seed, is_last_iteration| #inner_fn_name(#inner_fn_args),
|
||||||
|
stringify!(#outer_fn_name).to_string(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -554,6 +554,15 @@ pub struct Editor {
|
||||||
pub link_definition: HighlightStyle,
|
pub link_definition: HighlightStyle,
|
||||||
pub composition_mark: HighlightStyle,
|
pub composition_mark: HighlightStyle,
|
||||||
pub jump_icon: Interactive<IconButton>,
|
pub jump_icon: Interactive<IconButton>,
|
||||||
|
pub scrollbar: Scrollbar,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Default)]
|
||||||
|
pub struct Scrollbar {
|
||||||
|
pub track: ContainerStyle,
|
||||||
|
pub thumb: ContainerStyle,
|
||||||
|
pub width: f32,
|
||||||
|
pub min_height_factor: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Default)]
|
#[derive(Clone, Deserialize, Default)]
|
||||||
|
|
|
@ -7,7 +7,20 @@ edition = "2021"
|
||||||
path = "src/vim.rs"
|
path = "src/vim.rs"
|
||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
|
[features]
|
||||||
|
neovim = ["nvim-rs", "async-compat", "async-trait", "tokio"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||||
|
itertools = "0.10"
|
||||||
|
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||||
|
|
||||||
|
async-compat = { version = "0.2.1", "optional" = true }
|
||||||
|
async-trait = { version = "0.1", "optional" = true }
|
||||||
|
nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", branch = "master", features = ["use_tokio"], optional = true }
|
||||||
|
tokio = { version = "1.15", "optional" = true }
|
||||||
|
serde_json = { version = "1.0", features = ["preserve_order"] }
|
||||||
|
|
||||||
assets = { path = "../assets" }
|
assets = { path = "../assets" }
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
command_palette = { path = "../command_palette" }
|
command_palette = { path = "../command_palette" }
|
||||||
|
@ -16,14 +29,14 @@ gpui = { path = "../gpui" }
|
||||||
language = { path = "../language" }
|
language = { path = "../language" }
|
||||||
rope = { path = "../rope" }
|
rope = { path = "../rope" }
|
||||||
search = { path = "../search" }
|
search = { path = "../search" }
|
||||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
workspace = { path = "../workspace" }
|
workspace = { path = "../workspace" }
|
||||||
itertools = "0.10"
|
|
||||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
indoc = "1.0.4"
|
indoc = "1.0.4"
|
||||||
|
parking_lot = "0.11.1"
|
||||||
|
lazy_static = "1.4"
|
||||||
|
|
||||||
editor = { path = "../editor", features = ["test-support"] }
|
editor = { path = "../editor", features = ["test-support"] }
|
||||||
gpui = { path = "../gpui", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
language = { path = "../language", features = ["test-support"] }
|
language = { path = "../language", features = ["test-support"] }
|
||||||
|
|
|
@ -26,7 +26,7 @@ fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext<Works
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::{state::Mode, vim_test_context::VimTestContext};
|
use crate::{state::Mode, test::VimTestContext};
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_enter_and_exit_insert_mode(cx: &mut gpui::TestAppContext) {
|
async fn test_enter_and_exit_insert_mode(cx: &mut gpui::TestAppContext) {
|
||||||
|
|
|
@ -18,6 +18,7 @@ use crate::{
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub enum Motion {
|
pub enum Motion {
|
||||||
Left,
|
Left,
|
||||||
|
Backspace,
|
||||||
Down,
|
Down,
|
||||||
Up,
|
Up,
|
||||||
Right,
|
Right,
|
||||||
|
@ -58,6 +59,7 @@ actions!(
|
||||||
vim,
|
vim,
|
||||||
[
|
[
|
||||||
Left,
|
Left,
|
||||||
|
Backspace,
|
||||||
Down,
|
Down,
|
||||||
Up,
|
Up,
|
||||||
Right,
|
Right,
|
||||||
|
@ -74,6 +76,7 @@ impl_actions!(vim, [NextWordStart, NextWordEnd, PreviousWordStart]);
|
||||||
|
|
||||||
pub fn init(cx: &mut MutableAppContext) {
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
cx.add_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
|
cx.add_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
|
||||||
|
cx.add_action(|_: &mut Workspace, _: &Backspace, cx: _| motion(Motion::Backspace, cx));
|
||||||
cx.add_action(|_: &mut Workspace, _: &Down, cx: _| motion(Motion::Down, cx));
|
cx.add_action(|_: &mut Workspace, _: &Down, cx: _| motion(Motion::Down, cx));
|
||||||
cx.add_action(|_: &mut Workspace, _: &Up, cx: _| motion(Motion::Up, cx));
|
cx.add_action(|_: &mut Workspace, _: &Up, cx: _| motion(Motion::Up, cx));
|
||||||
cx.add_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
|
cx.add_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
|
||||||
|
@ -106,19 +109,21 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn motion(motion: Motion, cx: &mut MutableAppContext) {
|
pub(crate) fn motion(motion: Motion, cx: &mut MutableAppContext) {
|
||||||
Vim::update(cx, |vim, cx| {
|
if let Some(Operator::Namespace(_)) = Vim::read(cx).active_operator() {
|
||||||
if let Some(Operator::Namespace(_)) = vim.active_operator() {
|
Vim::update(cx, |vim, cx| vim.pop_operator(cx));
|
||||||
vim.pop_operator(cx);
|
}
|
||||||
}
|
|
||||||
});
|
let times = Vim::update(cx, |vim, cx| vim.pop_number_operator(cx));
|
||||||
|
let operator = Vim::read(cx).active_operator();
|
||||||
match Vim::read(cx).state.mode {
|
match Vim::read(cx).state.mode {
|
||||||
Mode::Normal => normal_motion(motion, cx),
|
Mode::Normal => normal_motion(motion, operator, times, cx),
|
||||||
Mode::Visual { .. } => visual_motion(motion, cx),
|
Mode::Visual { .. } => visual_motion(motion, times, cx),
|
||||||
Mode::Insert => {
|
Mode::Insert => {
|
||||||
// Shouldn't execute a motion in insert mode. Ignoring
|
// Shouldn't execute a motion in insert mode. Ignoring
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Vim::update(cx, |vim, cx| vim.clear_operator(cx));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Motion handling is specified here:
|
// Motion handling is specified here:
|
||||||
|
@ -150,30 +155,32 @@ impl Motion {
|
||||||
map: &DisplaySnapshot,
|
map: &DisplaySnapshot,
|
||||||
point: DisplayPoint,
|
point: DisplayPoint,
|
||||||
goal: SelectionGoal,
|
goal: SelectionGoal,
|
||||||
|
times: usize,
|
||||||
) -> (DisplayPoint, SelectionGoal) {
|
) -> (DisplayPoint, SelectionGoal) {
|
||||||
use Motion::*;
|
use Motion::*;
|
||||||
match self {
|
match self {
|
||||||
Left => (left(map, point), SelectionGoal::None),
|
Left => (left(map, point, times), SelectionGoal::None),
|
||||||
Down => movement::down(map, point, goal, true),
|
Backspace => (backspace(map, point, times), SelectionGoal::None),
|
||||||
Up => movement::up(map, point, goal, true),
|
Down => down(map, point, goal, times),
|
||||||
Right => (right(map, point), SelectionGoal::None),
|
Up => up(map, point, goal, times),
|
||||||
|
Right => (right(map, point, times), SelectionGoal::None),
|
||||||
NextWordStart { ignore_punctuation } => (
|
NextWordStart { ignore_punctuation } => (
|
||||||
next_word_start(map, point, ignore_punctuation),
|
next_word_start(map, point, ignore_punctuation, times),
|
||||||
SelectionGoal::None,
|
SelectionGoal::None,
|
||||||
),
|
),
|
||||||
NextWordEnd { ignore_punctuation } => (
|
NextWordEnd { ignore_punctuation } => (
|
||||||
next_word_end(map, point, ignore_punctuation),
|
next_word_end(map, point, ignore_punctuation, times),
|
||||||
SelectionGoal::None,
|
SelectionGoal::None,
|
||||||
),
|
),
|
||||||
PreviousWordStart { ignore_punctuation } => (
|
PreviousWordStart { ignore_punctuation } => (
|
||||||
previous_word_start(map, point, ignore_punctuation),
|
previous_word_start(map, point, ignore_punctuation, times),
|
||||||
SelectionGoal::None,
|
SelectionGoal::None,
|
||||||
),
|
),
|
||||||
FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None),
|
FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None),
|
||||||
StartOfLine => (start_of_line(map, point), SelectionGoal::None),
|
StartOfLine => (start_of_line(map, point), SelectionGoal::None),
|
||||||
EndOfLine => (end_of_line(map, point), SelectionGoal::None),
|
EndOfLine => (end_of_line(map, point), SelectionGoal::None),
|
||||||
CurrentLine => (end_of_line(map, point), SelectionGoal::None),
|
CurrentLine => (end_of_line(map, point), SelectionGoal::None),
|
||||||
StartOfDocument => (start_of_document(map, point), SelectionGoal::None),
|
StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
|
||||||
EndOfDocument => (end_of_document(map, point), SelectionGoal::None),
|
EndOfDocument => (end_of_document(map, point), SelectionGoal::None),
|
||||||
Matching => (matching(map, point), SelectionGoal::None),
|
Matching => (matching(map, point), SelectionGoal::None),
|
||||||
}
|
}
|
||||||
|
@ -184,9 +191,10 @@ impl Motion {
|
||||||
self,
|
self,
|
||||||
map: &DisplaySnapshot,
|
map: &DisplaySnapshot,
|
||||||
selection: &mut Selection<DisplayPoint>,
|
selection: &mut Selection<DisplayPoint>,
|
||||||
|
times: usize,
|
||||||
expand_to_surrounding_newline: bool,
|
expand_to_surrounding_newline: bool,
|
||||||
) {
|
) {
|
||||||
let (head, goal) = self.move_point(map, selection.head(), selection.goal);
|
let (head, goal) = self.move_point(map, selection.head(), selection.goal, times);
|
||||||
selection.set_head(head, goal);
|
selection.set_head(head, goal);
|
||||||
|
|
||||||
if self.linewise() {
|
if self.linewise() {
|
||||||
|
@ -206,7 +214,7 @@ impl Motion {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
|
(_, selection.end) = map.next_line_boundary(selection.end.to_point(map));
|
||||||
} else {
|
} else {
|
||||||
// If the motion is exclusive and the end of the motion is in column 1, the
|
// If the motion is exclusive and the end of the motion is in column 1, the
|
||||||
// end of the motion is moved to the end of the previous line and the motion
|
// end of the motion is moved to the end of the previous line and the motion
|
||||||
|
@ -234,95 +242,151 @@ impl Motion {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
|
fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
|
||||||
*point.column_mut() = point.column().saturating_sub(1);
|
for _ in 0..times {
|
||||||
map.clip_point(point, Bias::Left)
|
*point.column_mut() = point.column().saturating_sub(1);
|
||||||
}
|
point = map.clip_point(point, Bias::Right);
|
||||||
|
if point.column() == 0 {
|
||||||
fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
|
break;
|
||||||
*point.column_mut() += 1;
|
|
||||||
map.clip_point(point, Bias::Right)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next_word_start(
|
|
||||||
map: &DisplaySnapshot,
|
|
||||||
point: DisplayPoint,
|
|
||||||
ignore_punctuation: bool,
|
|
||||||
) -> DisplayPoint {
|
|
||||||
let mut crossed_newline = false;
|
|
||||||
movement::find_boundary(map, point, |left, right| {
|
|
||||||
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
|
|
||||||
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
|
|
||||||
let at_newline = right == '\n';
|
|
||||||
|
|
||||||
let found = (left_kind != right_kind && !right.is_whitespace())
|
|
||||||
|| at_newline && crossed_newline
|
|
||||||
|| at_newline && left == '\n'; // Prevents skipping repeated empty lines
|
|
||||||
|
|
||||||
if at_newline {
|
|
||||||
crossed_newline = true;
|
|
||||||
}
|
}
|
||||||
found
|
}
|
||||||
})
|
point
|
||||||
|
}
|
||||||
|
|
||||||
|
fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
|
||||||
|
for _ in 0..times {
|
||||||
|
point = movement::left(map, point);
|
||||||
|
}
|
||||||
|
point
|
||||||
|
}
|
||||||
|
|
||||||
|
fn down(
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
mut point: DisplayPoint,
|
||||||
|
mut goal: SelectionGoal,
|
||||||
|
times: usize,
|
||||||
|
) -> (DisplayPoint, SelectionGoal) {
|
||||||
|
for _ in 0..times {
|
||||||
|
(point, goal) = movement::down(map, point, goal, true);
|
||||||
|
}
|
||||||
|
(point, goal)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn up(
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
mut point: DisplayPoint,
|
||||||
|
mut goal: SelectionGoal,
|
||||||
|
times: usize,
|
||||||
|
) -> (DisplayPoint, SelectionGoal) {
|
||||||
|
for _ in 0..times {
|
||||||
|
(point, goal) = movement::up(map, point, goal, true);
|
||||||
|
}
|
||||||
|
(point, goal)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
|
||||||
|
for _ in 0..times {
|
||||||
|
let mut new_point = point;
|
||||||
|
*new_point.column_mut() += 1;
|
||||||
|
let new_point = map.clip_point(new_point, Bias::Right);
|
||||||
|
if point == new_point {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
point = new_point;
|
||||||
|
}
|
||||||
|
point
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn next_word_start(
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
mut point: DisplayPoint,
|
||||||
|
ignore_punctuation: bool,
|
||||||
|
times: usize,
|
||||||
|
) -> DisplayPoint {
|
||||||
|
for _ in 0..times {
|
||||||
|
let mut crossed_newline = false;
|
||||||
|
point = movement::find_boundary(map, point, |left, right| {
|
||||||
|
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
|
||||||
|
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
|
||||||
|
let at_newline = right == '\n';
|
||||||
|
|
||||||
|
let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
|
||||||
|
|| at_newline && crossed_newline
|
||||||
|
|| at_newline && left == '\n'; // Prevents skipping repeated empty lines
|
||||||
|
|
||||||
|
if at_newline {
|
||||||
|
crossed_newline = true;
|
||||||
|
}
|
||||||
|
found
|
||||||
|
})
|
||||||
|
}
|
||||||
|
point
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_word_end(
|
fn next_word_end(
|
||||||
map: &DisplaySnapshot,
|
map: &DisplaySnapshot,
|
||||||
mut point: DisplayPoint,
|
mut point: DisplayPoint,
|
||||||
ignore_punctuation: bool,
|
ignore_punctuation: bool,
|
||||||
|
times: usize,
|
||||||
) -> DisplayPoint {
|
) -> DisplayPoint {
|
||||||
*point.column_mut() += 1;
|
for _ in 0..times {
|
||||||
point = movement::find_boundary(map, point, |left, right| {
|
*point.column_mut() += 1;
|
||||||
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
|
point = movement::find_boundary(map, point, |left, right| {
|
||||||
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
|
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
|
||||||
|
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
|
||||||
|
|
||||||
left_kind != right_kind && !left.is_whitespace()
|
left_kind != right_kind && left_kind != CharKind::Whitespace
|
||||||
});
|
});
|
||||||
// find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
|
|
||||||
// we have backtraced already
|
// find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
|
||||||
if !map
|
// we have backtraced already
|
||||||
.chars_at(point)
|
if !map
|
||||||
.nth(1)
|
.chars_at(point)
|
||||||
.map(|c| c == '\n')
|
.nth(1)
|
||||||
.unwrap_or(true)
|
.map(|(c, _)| c == '\n')
|
||||||
{
|
.unwrap_or(true)
|
||||||
*point.column_mut() = point.column().saturating_sub(1);
|
{
|
||||||
|
*point.column_mut() = point.column().saturating_sub(1);
|
||||||
|
}
|
||||||
|
point = map.clip_point(point, Bias::Left);
|
||||||
}
|
}
|
||||||
map.clip_point(point, Bias::Left)
|
point
|
||||||
}
|
}
|
||||||
|
|
||||||
fn previous_word_start(
|
fn previous_word_start(
|
||||||
map: &DisplaySnapshot,
|
map: &DisplaySnapshot,
|
||||||
mut point: DisplayPoint,
|
mut point: DisplayPoint,
|
||||||
ignore_punctuation: bool,
|
ignore_punctuation: bool,
|
||||||
|
times: usize,
|
||||||
) -> DisplayPoint {
|
) -> DisplayPoint {
|
||||||
// This works even though find_preceding_boundary is called for every character in the line containing
|
for _ in 0..times {
|
||||||
// cursor because the newline is checked only once.
|
// This works even though find_preceding_boundary is called for every character in the line containing
|
||||||
point = movement::find_preceding_boundary(map, point, |left, right| {
|
// cursor because the newline is checked only once.
|
||||||
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
|
point = movement::find_preceding_boundary(map, point, |left, right| {
|
||||||
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
|
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
|
||||||
|
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
|
||||||
|
|
||||||
(left_kind != right_kind && !right.is_whitespace()) || left == '\n'
|
(left_kind != right_kind && !right.is_whitespace()) || left == '\n'
|
||||||
});
|
});
|
||||||
|
}
|
||||||
point
|
point
|
||||||
}
|
}
|
||||||
|
|
||||||
fn first_non_whitespace(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
|
fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoint {
|
||||||
let mut column = 0;
|
let mut last_point = DisplayPoint::new(from.row(), 0);
|
||||||
for ch in map.chars_at(DisplayPoint::new(point.row(), 0)) {
|
for (ch, point) in map.chars_at(last_point) {
|
||||||
if ch == '\n' {
|
if ch == '\n' {
|
||||||
return point;
|
return from;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
last_point = point;
|
||||||
|
|
||||||
if char_kind(ch) != CharKind::Whitespace {
|
if char_kind(ch) != CharKind::Whitespace {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
column += ch.len_utf8() as u32;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
*point.column_mut() = column;
|
map.clip_point(last_point, Bias::Left)
|
||||||
map.clip_point(point, Bias::Left)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
fn start_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
||||||
|
@ -333,8 +397,8 @@ fn end_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
||||||
map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
|
map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
|
||||||
let mut new_point = 0usize.to_display_point(map);
|
let mut new_point = (line - 1).to_display_point(map);
|
||||||
*new_point.column_mut() = point.column();
|
*new_point.column_mut() = point.column();
|
||||||
map.clip_point(new_point, Bias::Left)
|
map.clip_point(new_point, Bias::Left)
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,30 +1,20 @@
|
||||||
use crate::{motion::Motion, state::Mode, utils::copy_selections_content, Vim};
|
use crate::{motion::Motion, object::Object, state::Mode, utils::copy_selections_content, Vim};
|
||||||
use editor::{char_kind, movement, Autoscroll};
|
use editor::{char_kind, display_map::DisplaySnapshot, movement, Autoscroll, DisplayPoint};
|
||||||
use gpui::{impl_actions, MutableAppContext, ViewContext};
|
use gpui::MutableAppContext;
|
||||||
use serde::Deserialize;
|
use language::Selection;
|
||||||
use workspace::Workspace;
|
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, PartialEq)]
|
pub fn change_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut MutableAppContext) {
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
struct ChangeWord {
|
|
||||||
#[serde(default)]
|
|
||||||
ignore_punctuation: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl_actions!(vim, [ChangeWord]);
|
|
||||||
|
|
||||||
pub fn init(cx: &mut MutableAppContext) {
|
|
||||||
cx.add_action(change_word);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn change_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
|
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
editor.transact(cx, |editor, cx| {
|
editor.transact(cx, |editor, cx| {
|
||||||
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
|
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
|
||||||
editor.set_clip_at_line_ends(false, cx);
|
editor.set_clip_at_line_ends(false, cx);
|
||||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||||
s.move_with(|map, selection| {
|
s.move_with(|map, selection| {
|
||||||
motion.expand_selection(map, selection, false);
|
if let Motion::NextWordStart { ignore_punctuation } = motion {
|
||||||
|
expand_changed_word_selection(map, selection, times, ignore_punctuation);
|
||||||
|
} else {
|
||||||
|
motion.expand_selection(map, selection, times, false);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
copy_selections_content(editor, motion.linewise(), cx);
|
copy_selections_content(editor, motion.linewise(), cx);
|
||||||
|
@ -34,43 +24,60 @@ pub fn change_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
|
||||||
vim.switch_mode(Mode::Insert, false, cx)
|
vim.switch_mode(Mode::Insert, false, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut MutableAppContext) {
|
||||||
|
let mut objects_found = false;
|
||||||
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
|
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
|
||||||
|
editor.set_clip_at_line_ends(false, cx);
|
||||||
|
editor.transact(cx, |editor, cx| {
|
||||||
|
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||||
|
s.move_with(|map, selection| {
|
||||||
|
objects_found |= object.expand_selection(map, selection, around);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if objects_found {
|
||||||
|
copy_selections_content(editor, false, cx);
|
||||||
|
editor.insert("", cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if objects_found {
|
||||||
|
vim.switch_mode(Mode::Insert, false, cx);
|
||||||
|
} else {
|
||||||
|
vim.switch_mode(Mode::Normal, false, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// From the docs https://vimhelp.org/change.txt.html#cw
|
// From the docs https://vimhelp.org/change.txt.html#cw
|
||||||
// Special case: When the cursor is in a word, "cw" and "cW" do not include the
|
// Special case: When the cursor is in a word, "cw" and "cW" do not include the
|
||||||
// white space after a word, they only change up to the end of the word. This is
|
// white space after a word, they only change up to the end of the word. This is
|
||||||
// because Vim interprets "cw" as change-word, and a word does not include the
|
// because Vim interprets "cw" as change-word, and a word does not include the
|
||||||
// following white space.
|
// following white space.
|
||||||
fn change_word(
|
fn expand_changed_word_selection(
|
||||||
_: &mut Workspace,
|
map: &DisplaySnapshot,
|
||||||
&ChangeWord { ignore_punctuation }: &ChangeWord,
|
selection: &mut Selection<DisplayPoint>,
|
||||||
cx: &mut ViewContext<Workspace>,
|
times: usize,
|
||||||
|
ignore_punctuation: bool,
|
||||||
) {
|
) {
|
||||||
Vim::update(cx, |vim, cx| {
|
if times > 1 {
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
Motion::NextWordStart { ignore_punctuation }.expand_selection(
|
||||||
editor.transact(cx, |editor, cx| {
|
map,
|
||||||
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
|
selection,
|
||||||
editor.set_clip_at_line_ends(false, cx);
|
times - 1,
|
||||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
false,
|
||||||
s.move_with(|map, selection| {
|
);
|
||||||
if selection.end.column() == map.line_len(selection.end.row()) {
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
selection.end =
|
if times == 1 && selection.end.column() == map.line_len(selection.end.row()) {
|
||||||
movement::find_boundary(map, selection.end, |left, right| {
|
return;
|
||||||
let left_kind =
|
}
|
||||||
char_kind(left).coerce_punctuation(ignore_punctuation);
|
|
||||||
let right_kind =
|
|
||||||
char_kind(right).coerce_punctuation(ignore_punctuation);
|
|
||||||
|
|
||||||
left_kind != right_kind || left == '\n' || right == '\n'
|
selection.end = movement::find_boundary(map, selection.end, |left, right| {
|
||||||
});
|
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
|
||||||
});
|
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
|
||||||
});
|
|
||||||
copy_selections_content(editor, false, cx);
|
left_kind != right_kind || left == '\n' || right == '\n'
|
||||||
editor.insert("", cx);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
vim.switch_mode(Mode::Insert, false, cx);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +85,10 @@ fn change_word(
|
||||||
mod test {
|
mod test {
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
|
|
||||||
use crate::{state::Mode, vim_test_context::VimTestContext};
|
use crate::{
|
||||||
|
state::Mode,
|
||||||
|
test::{NeovimBackedTestContext, VimTestContext},
|
||||||
|
};
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_change_h(cx: &mut gpui::TestAppContext) {
|
async fn test_change_h(cx: &mut gpui::TestAppContext) {
|
||||||
|
@ -170,8 +180,7 @@ mod test {
|
||||||
test"},
|
test"},
|
||||||
indoc! {"
|
indoc! {"
|
||||||
Test test
|
Test test
|
||||||
ˇ
|
ˇ"},
|
||||||
test"},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut cx = cx.binding(["c", "shift-e"]);
|
let mut cx = cx.binding(["c", "shift-e"]);
|
||||||
|
@ -193,6 +202,7 @@ mod test {
|
||||||
Test ˇ
|
Test ˇ
|
||||||
test"},
|
test"},
|
||||||
);
|
);
|
||||||
|
println!("Marker");
|
||||||
cx.assert(
|
cx.assert(
|
||||||
indoc! {"
|
indoc! {"
|
||||||
Test test
|
Test test
|
||||||
|
@ -442,4 +452,85 @@ mod test {
|
||||||
the lazy"},
|
the lazy"},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_repeated_cj(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
|
||||||
|
for count in 1..=5 {
|
||||||
|
cx.assert_binding_matches_all(
|
||||||
|
["c", &count.to_string(), "j"],
|
||||||
|
indoc! {"
|
||||||
|
ˇThe quˇickˇ browˇn
|
||||||
|
ˇ
|
||||||
|
ˇfox ˇjumpsˇ-ˇoˇver
|
||||||
|
ˇthe lazy dog
|
||||||
|
"},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_repeated_cl(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
|
||||||
|
for count in 1..=5 {
|
||||||
|
cx.assert_binding_matches_all(
|
||||||
|
["c", &count.to_string(), "l"],
|
||||||
|
indoc! {"
|
||||||
|
ˇThe quˇickˇ browˇn
|
||||||
|
ˇ
|
||||||
|
ˇfox ˇjumpsˇ-ˇoˇver
|
||||||
|
ˇthe lazy dog
|
||||||
|
"},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_repeated_cb(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
|
||||||
|
// Changing back any number of times from the start of the file doesn't
|
||||||
|
// switch to insert mode in vim. This is weird and painful to implement
|
||||||
|
cx.add_initial_state_exemption(indoc! {"
|
||||||
|
ˇThe quick brown
|
||||||
|
|
||||||
|
fox jumps-over
|
||||||
|
the lazy dog
|
||||||
|
"});
|
||||||
|
|
||||||
|
for count in 1..=5 {
|
||||||
|
cx.assert_binding_matches_all(
|
||||||
|
["c", &count.to_string(), "b"],
|
||||||
|
indoc! {"
|
||||||
|
ˇThe quˇickˇ browˇn
|
||||||
|
ˇ
|
||||||
|
ˇfox ˇjumpsˇ-ˇoˇver
|
||||||
|
ˇthe lazy dog
|
||||||
|
"},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_repeated_ce(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
|
||||||
|
for count in 1..=5 {
|
||||||
|
cx.assert_binding_matches_all(
|
||||||
|
["c", &count.to_string(), "e"],
|
||||||
|
indoc! {"
|
||||||
|
ˇThe quˇickˇ browˇn
|
||||||
|
ˇ
|
||||||
|
ˇfox ˇjumpsˇ-ˇoˇver
|
||||||
|
ˇthe lazy dog
|
||||||
|
"},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
use crate::{motion::Motion, utils::copy_selections_content, Vim};
|
use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim};
|
||||||
use collections::HashMap;
|
use collections::{HashMap, HashSet};
|
||||||
use editor::{Autoscroll, Bias};
|
use editor::{display_map::ToDisplayPoint, Autoscroll, Bias};
|
||||||
use gpui::MutableAppContext;
|
use gpui::MutableAppContext;
|
||||||
|
|
||||||
pub fn delete_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
|
pub fn delete_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut MutableAppContext) {
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
editor.transact(cx, |editor, cx| {
|
editor.transact(cx, |editor, cx| {
|
||||||
editor.set_clip_at_line_ends(false, cx);
|
editor.set_clip_at_line_ends(false, cx);
|
||||||
|
@ -11,8 +11,8 @@ pub fn delete_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
|
||||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||||
s.move_with(|map, selection| {
|
s.move_with(|map, selection| {
|
||||||
let original_head = selection.head();
|
let original_head = selection.head();
|
||||||
motion.expand_selection(map, selection, true);
|
|
||||||
original_columns.insert(selection.id, original_head.column());
|
original_columns.insert(selection.id, original_head.column());
|
||||||
|
motion.expand_selection(map, selection, times, true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
copy_selections_content(editor, motion.linewise(), cx);
|
copy_selections_content(editor, motion.linewise(), cx);
|
||||||
|
@ -36,11 +36,67 @@ pub fn delete_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut MutableAppContext) {
|
||||||
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
|
editor.transact(cx, |editor, cx| {
|
||||||
|
editor.set_clip_at_line_ends(false, cx);
|
||||||
|
// Emulates behavior in vim where if we expanded backwards to include a newline
|
||||||
|
// the cursor gets set back to the start of the line
|
||||||
|
let mut should_move_to_start: HashSet<_> = Default::default();
|
||||||
|
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||||
|
s.move_with(|map, selection| {
|
||||||
|
object.expand_selection(map, selection, around);
|
||||||
|
let offset_range = selection.map(|p| p.to_offset(map, Bias::Left)).range();
|
||||||
|
let contains_only_newlines = map
|
||||||
|
.chars_at(selection.start)
|
||||||
|
.take_while(|(_, p)| p < &selection.end)
|
||||||
|
.all(|(char, _)| char == '\n')
|
||||||
|
&& !offset_range.is_empty();
|
||||||
|
let end_at_newline = map
|
||||||
|
.chars_at(selection.end)
|
||||||
|
.next()
|
||||||
|
.map(|(c, _)| c == '\n')
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
// If expanded range contains only newlines and
|
||||||
|
// the object is around or sentence, expand to include a newline
|
||||||
|
// at the end or start
|
||||||
|
if (around || object == Object::Sentence) && contains_only_newlines {
|
||||||
|
if end_at_newline {
|
||||||
|
selection.end =
|
||||||
|
(offset_range.end + '\n'.len_utf8()).to_display_point(map);
|
||||||
|
} else if selection.start.row() > 0 {
|
||||||
|
should_move_to_start.insert(selection.id);
|
||||||
|
selection.start =
|
||||||
|
(offset_range.start - '\n'.len_utf8()).to_display_point(map);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
copy_selections_content(editor, false, cx);
|
||||||
|
editor.insert("", cx);
|
||||||
|
|
||||||
|
// Fixup cursor position after the deletion
|
||||||
|
editor.set_clip_at_line_ends(true, cx);
|
||||||
|
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||||
|
s.move_with(|map, selection| {
|
||||||
|
let mut cursor = selection.head();
|
||||||
|
if should_move_to_start.contains(&selection.id) {
|
||||||
|
*cursor.column_mut() = 0;
|
||||||
|
}
|
||||||
|
cursor = map.clip_point(cursor, Bias::Left);
|
||||||
|
selection.collapse_to(cursor, selection.goal)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
|
|
||||||
use crate::{state::Mode, vim_test_context::VimTestContext};
|
use crate::{state::Mode, test::VimTestContext};
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_delete_h(cx: &mut gpui::TestAppContext) {
|
async fn test_delete_h(cx: &mut gpui::TestAppContext) {
|
||||||
|
@ -140,8 +196,7 @@ mod test {
|
||||||
test"},
|
test"},
|
||||||
indoc! {"
|
indoc! {"
|
||||||
Test test
|
Test test
|
||||||
ˇ
|
ˇ"},
|
||||||
test"},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut cx = cx.binding(["d", "shift-e"]);
|
let mut cx = cx.binding(["d", "shift-e"]);
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use crate::{motion::Motion, utils::copy_selections_content, Vim};
|
use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use gpui::MutableAppContext;
|
use gpui::MutableAppContext;
|
||||||
|
|
||||||
pub fn yank_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
|
pub fn yank_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut MutableAppContext) {
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
editor.transact(cx, |editor, cx| {
|
editor.transact(cx, |editor, cx| {
|
||||||
editor.set_clip_at_line_ends(false, cx);
|
editor.set_clip_at_line_ends(false, cx);
|
||||||
|
@ -10,8 +10,8 @@ pub fn yank_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
|
||||||
editor.change_selections(None, cx, |s| {
|
editor.change_selections(None, cx, |s| {
|
||||||
s.move_with(|map, selection| {
|
s.move_with(|map, selection| {
|
||||||
let original_position = (selection.head(), selection.goal);
|
let original_position = (selection.head(), selection.goal);
|
||||||
motion.expand_selection(map, selection, true);
|
|
||||||
original_positions.insert(selection.id, original_position);
|
original_positions.insert(selection.id, original_position);
|
||||||
|
motion.expand_selection(map, selection, times, true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
copy_selections_content(editor, motion.linewise(), cx);
|
copy_selections_content(editor, motion.linewise(), cx);
|
||||||
|
@ -24,3 +24,26 @@ pub fn yank_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn yank_object(vim: &mut Vim, object: Object, around: bool, cx: &mut MutableAppContext) {
|
||||||
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
|
editor.transact(cx, |editor, cx| {
|
||||||
|
editor.set_clip_at_line_ends(false, cx);
|
||||||
|
let mut original_positions: HashMap<_, _> = Default::default();
|
||||||
|
editor.change_selections(None, cx, |s| {
|
||||||
|
s.move_with(|map, selection| {
|
||||||
|
let original_position = (selection.head(), selection.goal);
|
||||||
|
object.expand_selection(map, selection, around);
|
||||||
|
original_positions.insert(selection.id, original_position);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
copy_selections_content(editor, false, cx);
|
||||||
|
editor.change_selections(None, cx, |s| {
|
||||||
|
s.move_with(|_, selection| {
|
||||||
|
let (head, goal) = original_positions.remove(&selection.id).unwrap();
|
||||||
|
selection.collapse_to(head, goal);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
640
crates/vim/src/object.rs
Normal file
640
crates/vim/src/object.rs
Normal file
|
@ -0,0 +1,640 @@
|
||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
use editor::{char_kind, display_map::DisplaySnapshot, movement, Bias, CharKind, DisplayPoint};
|
||||||
|
use gpui::{actions, impl_actions, MutableAppContext};
|
||||||
|
use language::Selection;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
use crate::{motion::right, normal::normal_object, state::Mode, visual::visual_object, Vim};
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
|
pub enum Object {
|
||||||
|
Word { ignore_punctuation: bool },
|
||||||
|
Sentence,
|
||||||
|
Quotes,
|
||||||
|
BackQuotes,
|
||||||
|
DoubleQuotes,
|
||||||
|
Parentheses,
|
||||||
|
SquareBrackets,
|
||||||
|
CurlyBrackets,
|
||||||
|
AngleBrackets,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct Word {
|
||||||
|
#[serde(default)]
|
||||||
|
ignore_punctuation: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
actions!(
|
||||||
|
vim,
|
||||||
|
[
|
||||||
|
Sentence,
|
||||||
|
Quotes,
|
||||||
|
BackQuotes,
|
||||||
|
DoubleQuotes,
|
||||||
|
Parentheses,
|
||||||
|
SquareBrackets,
|
||||||
|
CurlyBrackets,
|
||||||
|
AngleBrackets
|
||||||
|
]
|
||||||
|
);
|
||||||
|
impl_actions!(vim, [Word]);
|
||||||
|
|
||||||
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
|
cx.add_action(
|
||||||
|
|_: &mut Workspace, &Word { ignore_punctuation }: &Word, cx: _| {
|
||||||
|
object(Object::Word { ignore_punctuation }, cx)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
cx.add_action(|_: &mut Workspace, _: &Sentence, cx: _| object(Object::Sentence, cx));
|
||||||
|
cx.add_action(|_: &mut Workspace, _: &Quotes, cx: _| object(Object::Quotes, cx));
|
||||||
|
cx.add_action(|_: &mut Workspace, _: &BackQuotes, cx: _| object(Object::BackQuotes, cx));
|
||||||
|
cx.add_action(|_: &mut Workspace, _: &DoubleQuotes, cx: _| object(Object::DoubleQuotes, cx));
|
||||||
|
cx.add_action(|_: &mut Workspace, _: &Parentheses, cx: _| object(Object::Parentheses, cx));
|
||||||
|
cx.add_action(|_: &mut Workspace, _: &SquareBrackets, cx: _| {
|
||||||
|
object(Object::SquareBrackets, cx)
|
||||||
|
});
|
||||||
|
cx.add_action(|_: &mut Workspace, _: &CurlyBrackets, cx: _| object(Object::CurlyBrackets, cx));
|
||||||
|
cx.add_action(|_: &mut Workspace, _: &AngleBrackets, cx: _| object(Object::AngleBrackets, cx));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn object(object: Object, cx: &mut MutableAppContext) {
|
||||||
|
match Vim::read(cx).state.mode {
|
||||||
|
Mode::Normal => normal_object(object, cx),
|
||||||
|
Mode::Visual { .. } => visual_object(object, cx),
|
||||||
|
Mode::Insert => {
|
||||||
|
// Shouldn't execute a text object in insert mode. Ignoring
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Object {
|
||||||
|
pub fn range(
|
||||||
|
self,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
relative_to: DisplayPoint,
|
||||||
|
around: bool,
|
||||||
|
) -> Option<Range<DisplayPoint>> {
|
||||||
|
match self {
|
||||||
|
Object::Word { ignore_punctuation } => {
|
||||||
|
if around {
|
||||||
|
around_word(map, relative_to, ignore_punctuation)
|
||||||
|
} else {
|
||||||
|
in_word(map, relative_to, ignore_punctuation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Object::Sentence => sentence(map, relative_to, around),
|
||||||
|
Object::Quotes => surrounding_markers(map, relative_to, around, false, '\'', '\''),
|
||||||
|
Object::BackQuotes => surrounding_markers(map, relative_to, around, false, '`', '`'),
|
||||||
|
Object::DoubleQuotes => surrounding_markers(map, relative_to, around, false, '"', '"'),
|
||||||
|
Object::Parentheses => surrounding_markers(map, relative_to, around, true, '(', ')'),
|
||||||
|
Object::SquareBrackets => surrounding_markers(map, relative_to, around, true, '[', ']'),
|
||||||
|
Object::CurlyBrackets => surrounding_markers(map, relative_to, around, true, '{', '}'),
|
||||||
|
Object::AngleBrackets => surrounding_markers(map, relative_to, around, true, '<', '>'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expand_selection(
|
||||||
|
self,
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
selection: &mut Selection<DisplayPoint>,
|
||||||
|
around: bool,
|
||||||
|
) -> bool {
|
||||||
|
if let Some(range) = self.range(map, selection.head(), around) {
|
||||||
|
selection.start = range.start;
|
||||||
|
selection.end = range.end;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a range that surrounds the word relative_to is in
|
||||||
|
/// If relative_to is at the start of a word, return the word.
|
||||||
|
/// If relative_to is between words, return the space between
|
||||||
|
fn in_word(
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
relative_to: DisplayPoint,
|
||||||
|
ignore_punctuation: bool,
|
||||||
|
) -> Option<Range<DisplayPoint>> {
|
||||||
|
// Use motion::right so that we consider the character under the cursor when looking for the start
|
||||||
|
let start = movement::find_preceding_boundary_in_line(
|
||||||
|
map,
|
||||||
|
right(map, relative_to, 1),
|
||||||
|
|left, right| {
|
||||||
|
char_kind(left).coerce_punctuation(ignore_punctuation)
|
||||||
|
!= char_kind(right).coerce_punctuation(ignore_punctuation)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let end = movement::find_boundary_in_line(map, relative_to, |left, right| {
|
||||||
|
char_kind(left).coerce_punctuation(ignore_punctuation)
|
||||||
|
!= char_kind(right).coerce_punctuation(ignore_punctuation)
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(start..end)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a range that surrounds the word and following whitespace
|
||||||
|
/// relative_to is in.
|
||||||
|
/// If relative_to is at the start of a word, return the word and following whitespace.
|
||||||
|
/// If relative_to is between words, return the whitespace back and the following word
|
||||||
|
|
||||||
|
/// if in word
|
||||||
|
/// delete that word
|
||||||
|
/// if there is whitespace following the word, delete that as well
|
||||||
|
/// otherwise, delete any preceding whitespace
|
||||||
|
/// otherwise
|
||||||
|
/// delete whitespace around cursor
|
||||||
|
/// delete word following the cursor
|
||||||
|
fn around_word(
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
relative_to: DisplayPoint,
|
||||||
|
ignore_punctuation: bool,
|
||||||
|
) -> Option<Range<DisplayPoint>> {
|
||||||
|
let in_word = map
|
||||||
|
.chars_at(relative_to)
|
||||||
|
.next()
|
||||||
|
.map(|(c, _)| char_kind(c) != CharKind::Whitespace)
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if in_word {
|
||||||
|
around_containing_word(map, relative_to, ignore_punctuation)
|
||||||
|
} else {
|
||||||
|
around_next_word(map, relative_to, ignore_punctuation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn around_containing_word(
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
relative_to: DisplayPoint,
|
||||||
|
ignore_punctuation: bool,
|
||||||
|
) -> Option<Range<DisplayPoint>> {
|
||||||
|
in_word(map, relative_to, ignore_punctuation)
|
||||||
|
.map(|range| expand_to_include_whitespace(map, range, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn around_next_word(
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
relative_to: DisplayPoint,
|
||||||
|
ignore_punctuation: bool,
|
||||||
|
) -> Option<Range<DisplayPoint>> {
|
||||||
|
// Get the start of the word
|
||||||
|
let start = movement::find_preceding_boundary_in_line(
|
||||||
|
map,
|
||||||
|
right(map, relative_to, 1),
|
||||||
|
|left, right| {
|
||||||
|
char_kind(left).coerce_punctuation(ignore_punctuation)
|
||||||
|
!= char_kind(right).coerce_punctuation(ignore_punctuation)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut word_found = false;
|
||||||
|
let end = movement::find_boundary(map, relative_to, |left, right| {
|
||||||
|
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
|
||||||
|
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
|
||||||
|
|
||||||
|
let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n';
|
||||||
|
|
||||||
|
if right_kind != CharKind::Whitespace {
|
||||||
|
word_found = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
found
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(start..end)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sentence(
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
relative_to: DisplayPoint,
|
||||||
|
around: bool,
|
||||||
|
) -> Option<Range<DisplayPoint>> {
|
||||||
|
let mut start = None;
|
||||||
|
let mut previous_end = relative_to;
|
||||||
|
|
||||||
|
let mut chars = map.chars_at(relative_to).peekable();
|
||||||
|
|
||||||
|
// Search backwards for the previous sentence end or current sentence start. Include the character under relative_to
|
||||||
|
for (char, point) in chars
|
||||||
|
.peek()
|
||||||
|
.cloned()
|
||||||
|
.into_iter()
|
||||||
|
.chain(map.reverse_chars_at(relative_to))
|
||||||
|
{
|
||||||
|
if is_sentence_end(map, point) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_possible_sentence_start(char) {
|
||||||
|
start = Some(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
previous_end = point;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search forward for the end of the current sentence or if we are between sentences, the start of the next one
|
||||||
|
let mut end = relative_to;
|
||||||
|
for (char, point) in chars {
|
||||||
|
if start.is_none() && is_possible_sentence_start(char) {
|
||||||
|
if around {
|
||||||
|
start = Some(point);
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
end = point;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
end = point;
|
||||||
|
*end.column_mut() += char.len_utf8() as u32;
|
||||||
|
end = map.clip_point(end, Bias::Left);
|
||||||
|
|
||||||
|
if is_sentence_end(map, end) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut range = start.unwrap_or(previous_end)..end;
|
||||||
|
if around {
|
||||||
|
range = expand_to_include_whitespace(map, range, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(range)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_possible_sentence_start(character: char) -> bool {
|
||||||
|
!character.is_whitespace() && character != '.'
|
||||||
|
}
|
||||||
|
|
||||||
|
const SENTENCE_END_PUNCTUATION: &[char] = &['.', '!', '?'];
|
||||||
|
const SENTENCE_END_FILLERS: &[char] = &[')', ']', '"', '\''];
|
||||||
|
const SENTENCE_END_WHITESPACE: &[char] = &[' ', '\t', '\n'];
|
||||||
|
fn is_sentence_end(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
|
||||||
|
let mut next_chars = map.chars_at(point).peekable();
|
||||||
|
if let Some((char, _)) = next_chars.next() {
|
||||||
|
// We are at a double newline. This position is a sentence end.
|
||||||
|
if char == '\n' && next_chars.peek().map(|(c, _)| c == &'\n').unwrap_or(false) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The next text is not a valid whitespace. This is not a sentence end
|
||||||
|
if !SENTENCE_END_WHITESPACE.contains(&char) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (char, _) in map.reverse_chars_at(point) {
|
||||||
|
if SENTENCE_END_PUNCTUATION.contains(&char) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !SENTENCE_END_FILLERS.contains(&char) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Expands the passed range to include whitespace on one side or the other in a line. Attempts to add the
|
||||||
|
/// whitespace to the end first and falls back to the start if there was none.
|
||||||
|
fn expand_to_include_whitespace(
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
mut range: Range<DisplayPoint>,
|
||||||
|
stop_at_newline: bool,
|
||||||
|
) -> Range<DisplayPoint> {
|
||||||
|
let mut whitespace_included = false;
|
||||||
|
|
||||||
|
let mut chars = map.chars_at(range.end).peekable();
|
||||||
|
while let Some((char, point)) = chars.next() {
|
||||||
|
if char == '\n' && stop_at_newline {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if char.is_whitespace() {
|
||||||
|
// Set end to the next display_point or the character position after the current display_point
|
||||||
|
range.end = chars.peek().map(|(_, point)| *point).unwrap_or_else(|| {
|
||||||
|
let mut end = point;
|
||||||
|
*end.column_mut() += char.len_utf8() as u32;
|
||||||
|
map.clip_point(end, Bias::Left)
|
||||||
|
});
|
||||||
|
|
||||||
|
if char != '\n' {
|
||||||
|
whitespace_included = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Found non whitespace. Quit out.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !whitespace_included {
|
||||||
|
for (char, point) in map.reverse_chars_at(range.start) {
|
||||||
|
if char == '\n' && stop_at_newline {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !char.is_whitespace() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
range.start = point;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
range
|
||||||
|
}
|
||||||
|
|
||||||
|
fn surrounding_markers(
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
relative_to: DisplayPoint,
|
||||||
|
around: bool,
|
||||||
|
search_across_lines: bool,
|
||||||
|
start_marker: char,
|
||||||
|
end_marker: char,
|
||||||
|
) -> Option<Range<DisplayPoint>> {
|
||||||
|
let mut matched_ends = 0;
|
||||||
|
let mut start = None;
|
||||||
|
for (char, mut point) in map.reverse_chars_at(relative_to) {
|
||||||
|
if char == start_marker {
|
||||||
|
if matched_ends > 0 {
|
||||||
|
matched_ends -= 1;
|
||||||
|
} else {
|
||||||
|
if around {
|
||||||
|
start = Some(point)
|
||||||
|
} else {
|
||||||
|
*point.column_mut() += char.len_utf8() as u32;
|
||||||
|
start = Some(point);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if char == end_marker {
|
||||||
|
matched_ends += 1;
|
||||||
|
} else if char == '\n' && !search_across_lines {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut matched_starts = 0;
|
||||||
|
let mut end = None;
|
||||||
|
for (char, mut point) in map.chars_at(relative_to) {
|
||||||
|
if char == end_marker {
|
||||||
|
if start.is_none() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if matched_starts > 0 {
|
||||||
|
matched_starts -= 1;
|
||||||
|
} else {
|
||||||
|
if around {
|
||||||
|
*point.column_mut() += char.len_utf8() as u32;
|
||||||
|
end = Some(point);
|
||||||
|
} else {
|
||||||
|
end = Some(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if char == start_marker {
|
||||||
|
if start.is_none() {
|
||||||
|
if around {
|
||||||
|
start = Some(point);
|
||||||
|
} else {
|
||||||
|
*point.column_mut() += char.len_utf8() as u32;
|
||||||
|
start = Some(point);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
matched_starts += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if char == '\n' && !search_across_lines {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let (Some(start), Some(end)) = (start, end) {
|
||||||
|
Some(start..end)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use indoc::indoc;
|
||||||
|
|
||||||
|
use crate::test::NeovimBackedTestContext;
|
||||||
|
|
||||||
|
const WORD_LOCATIONS: &'static str = indoc! {"
|
||||||
|
The quick ˇbrowˇnˇ
|
||||||
|
fox ˇjuˇmpsˇ over
|
||||||
|
the lazy dogˇ
|
||||||
|
ˇ
|
||||||
|
ˇ
|
||||||
|
ˇ
|
||||||
|
Thˇeˇ-ˇquˇickˇ ˇbrownˇ
|
||||||
|
ˇ
|
||||||
|
ˇ
|
||||||
|
ˇ fox-jumpˇs over
|
||||||
|
the lazy dogˇ
|
||||||
|
ˇ
|
||||||
|
"};
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_change_word_object(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
|
||||||
|
cx.assert_binding_matches_all(["c", "i", "w"], WORD_LOCATIONS)
|
||||||
|
.await;
|
||||||
|
cx.assert_binding_matches_all(["c", "i", "shift-w"], WORD_LOCATIONS)
|
||||||
|
.await;
|
||||||
|
cx.assert_binding_matches_all(["c", "a", "w"], WORD_LOCATIONS)
|
||||||
|
.await;
|
||||||
|
cx.assert_binding_matches_all(["c", "a", "shift-w"], WORD_LOCATIONS)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_delete_word_object(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
|
||||||
|
cx.assert_binding_matches_all(["d", "i", "w"], WORD_LOCATIONS)
|
||||||
|
.await;
|
||||||
|
cx.assert_binding_matches_all(["d", "i", "shift-w"], WORD_LOCATIONS)
|
||||||
|
.await;
|
||||||
|
cx.assert_binding_matches_all(["d", "a", "w"], WORD_LOCATIONS)
|
||||||
|
.await;
|
||||||
|
cx.assert_binding_matches_all(["d", "a", "shift-w"], WORD_LOCATIONS)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_visual_word_object(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
|
||||||
|
cx.assert_binding_matches_all(["v", "i", "w"], WORD_LOCATIONS)
|
||||||
|
.await;
|
||||||
|
// Visual text objects are slightly broken when used with non empty selections
|
||||||
|
// cx.assert_binding_matches_all(["v", "h", "i", "w"], WORD_LOCATIONS)
|
||||||
|
// .await;
|
||||||
|
// cx.assert_binding_matches_all(["v", "l", "i", "w"], WORD_LOCATIONS)
|
||||||
|
// .await;
|
||||||
|
cx.assert_binding_matches_all(["v", "i", "shift-w"], WORD_LOCATIONS)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Visual text objects are slightly broken when used with non empty selections
|
||||||
|
// cx.assert_binding_matches_all(["v", "i", "h", "shift-w"], WORD_LOCATIONS)
|
||||||
|
// .await;
|
||||||
|
// cx.assert_binding_matches_all(["v", "i", "l", "shift-w"], WORD_LOCATIONS)
|
||||||
|
// .await;
|
||||||
|
|
||||||
|
// Visual around words is somewhat broken right now when it comes to newlines
|
||||||
|
// cx.assert_binding_matches_all(["v", "a", "w"], WORD_LOCATIONS)
|
||||||
|
// .await;
|
||||||
|
// cx.assert_binding_matches_all(["v", "a", "shift-w"], WORD_LOCATIONS)
|
||||||
|
// .await;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SENTENCE_EXAMPLES: &[&'static str] = &[
|
||||||
|
"ˇThe quick ˇbrownˇ?ˇ ˇFox Jˇumpsˇ!ˇ Ovˇer theˇ lazyˇ.",
|
||||||
|
indoc! {"
|
||||||
|
ˇThe quick ˇbrownˇ
|
||||||
|
fox jumps over
|
||||||
|
the lazy doˇgˇ.ˇ ˇThe quick ˇ
|
||||||
|
brown fox jumps over
|
||||||
|
"},
|
||||||
|
// Position of the cursor after deletion between lines isn't quite right.
|
||||||
|
// Deletion in a sentence at the start of a line with whitespace is incorrect.
|
||||||
|
// indoc! {"
|
||||||
|
// The quick brown fox jumps.
|
||||||
|
// Over the lazy dog
|
||||||
|
// ˇ
|
||||||
|
// ˇ
|
||||||
|
// ˇ fox-jumpˇs over
|
||||||
|
// the lazy dog.ˇ
|
||||||
|
// ˇ
|
||||||
|
// "},
|
||||||
|
r#"ˇThe ˇquick brownˇ.)ˇ]ˇ'ˇ" Brown ˇfox jumpsˇ.ˇ "#,
|
||||||
|
];
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_change_sentence_object(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx)
|
||||||
|
.await
|
||||||
|
.binding(["c", "i", "s"]);
|
||||||
|
for sentence_example in SENTENCE_EXAMPLES {
|
||||||
|
cx.assert_all(sentence_example).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cx = cx.binding(["c", "a", "s"]);
|
||||||
|
// Resulting position is slightly incorrect for unintuitive reasons.
|
||||||
|
cx.add_initial_state_exemption("The quick brown?ˇ Fox Jumps! Over the lazy.");
|
||||||
|
// Changing around the sentence at the end of the line doesn't remove whitespace.'
|
||||||
|
cx.add_initial_state_exemption("The quick brown.)]\'\" Brown fox jumps.ˇ ");
|
||||||
|
|
||||||
|
for sentence_example in SENTENCE_EXAMPLES {
|
||||||
|
cx.assert_all(sentence_example).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_delete_sentence_object(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx)
|
||||||
|
.await
|
||||||
|
.binding(["d", "i", "s"]);
|
||||||
|
for sentence_example in SENTENCE_EXAMPLES {
|
||||||
|
cx.assert_all(sentence_example).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cx = cx.binding(["d", "a", "s"]);
|
||||||
|
// Resulting position is slightly incorrect for unintuitive reasons.
|
||||||
|
cx.add_initial_state_exemption("The quick brown?ˇ Fox Jumps! Over the lazy.");
|
||||||
|
// Changing around the sentence at the end of the line doesn't remove whitespace.'
|
||||||
|
cx.add_initial_state_exemption("The quick brown.)]\'\" Brown fox jumps.ˇ ");
|
||||||
|
|
||||||
|
for sentence_example in SENTENCE_EXAMPLES {
|
||||||
|
cx.assert_all(sentence_example).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_visual_sentence_object(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx)
|
||||||
|
.await
|
||||||
|
.binding(["v", "i", "s"]);
|
||||||
|
for sentence_example in SENTENCE_EXAMPLES {
|
||||||
|
cx.assert_all(sentence_example).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visual around sentences is somewhat broken right now when it comes to newlines
|
||||||
|
// let mut cx = cx.binding(["d", "a", "s"]);
|
||||||
|
// for sentence_example in SENTENCE_EXAMPLES {
|
||||||
|
// cx.assert_all(sentence_example).await;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test string with "`" for opening surrounders and "'" for closing surrounders
|
||||||
|
const SURROUNDING_MARKER_STRING: &str = indoc! {"
|
||||||
|
ˇTh'ˇe ˇ`ˇ'ˇquˇi`ˇck broˇ'wn`
|
||||||
|
'ˇfox juˇmps ovˇ`ˇer
|
||||||
|
the ˇlazy dˇ'ˇoˇ`ˇg"};
|
||||||
|
|
||||||
|
const SURROUNDING_OBJECTS: &[(char, char)] = &[
|
||||||
|
// ('\'', '\''), // Quote,
|
||||||
|
// ('`', '`'), // Back Quote
|
||||||
|
// ('"', '"'), // Double Quote
|
||||||
|
// ('"', '"'), // Double Quote
|
||||||
|
('(', ')'), // Parentheses
|
||||||
|
('[', ']'), // SquareBrackets
|
||||||
|
('{', '}'), // CurlyBrackets
|
||||||
|
('<', '>'), // AngleBrackets
|
||||||
|
];
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_change_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
|
||||||
|
for (start, end) in SURROUNDING_OBJECTS {
|
||||||
|
let marked_string = SURROUNDING_MARKER_STRING
|
||||||
|
.replace('`', &start.to_string())
|
||||||
|
.replace('\'', &end.to_string());
|
||||||
|
|
||||||
|
// cx.assert_binding_matches_all(["c", "i", &start.to_string()], &marked_string)
|
||||||
|
// .await;
|
||||||
|
cx.assert_binding_matches_all(["c", "i", &end.to_string()], &marked_string)
|
||||||
|
.await;
|
||||||
|
// cx.assert_binding_matches_all(["c", "a", &start.to_string()], &marked_string)
|
||||||
|
// .await;
|
||||||
|
cx.assert_binding_matches_all(["c", "a", &end.to_string()], &marked_string)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
|
||||||
|
for (start, end) in SURROUNDING_OBJECTS {
|
||||||
|
let marked_string = SURROUNDING_MARKER_STRING
|
||||||
|
.replace('`', &start.to_string())
|
||||||
|
.replace('\'', &end.to_string());
|
||||||
|
|
||||||
|
// cx.assert_binding_matches_all(["d", "i", &start.to_string()], &marked_string)
|
||||||
|
// .await;
|
||||||
|
cx.assert_binding_matches_all(["d", "i", &end.to_string()], &marked_string)
|
||||||
|
.await;
|
||||||
|
// cx.assert_binding_matches_all(["d", "a", &start.to_string()], &marked_string)
|
||||||
|
// .await;
|
||||||
|
cx.assert_binding_matches_all(["d", "a", &end.to_string()], &marked_string)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
use editor::CursorShape;
|
use editor::CursorShape;
|
||||||
use gpui::keymap::Context;
|
use gpui::keymap::Context;
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
pub enum Mode {
|
pub enum Mode {
|
||||||
Normal,
|
Normal,
|
||||||
Insert,
|
Insert,
|
||||||
|
@ -22,10 +22,12 @@ pub enum Namespace {
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
|
||||||
pub enum Operator {
|
pub enum Operator {
|
||||||
|
Number(usize),
|
||||||
Namespace(Namespace),
|
Namespace(Namespace),
|
||||||
Change,
|
Change,
|
||||||
Delete,
|
Delete,
|
||||||
Yank,
|
Yank,
|
||||||
|
Object { around: bool },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
@ -77,7 +79,12 @@ impl VimState {
|
||||||
context.set.insert("VimControl".to_string());
|
context.set.insert("VimControl".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
Operator::set_context(self.operator_stack.last(), &mut context);
|
let active_operator = self.operator_stack.last();
|
||||||
|
if matches!(active_operator, Some(Operator::Object { .. })) {
|
||||||
|
context.set.insert("VimObject".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
Operator::set_context(active_operator, &mut context);
|
||||||
|
|
||||||
context
|
context
|
||||||
}
|
}
|
||||||
|
@ -86,10 +93,14 @@ impl VimState {
|
||||||
impl Operator {
|
impl Operator {
|
||||||
pub fn set_context(operator: Option<&Operator>, context: &mut Context) {
|
pub fn set_context(operator: Option<&Operator>, context: &mut Context) {
|
||||||
let operator_context = match operator {
|
let operator_context = match operator {
|
||||||
|
Some(Operator::Number(_)) => "n",
|
||||||
Some(Operator::Namespace(Namespace::G)) => "g",
|
Some(Operator::Namespace(Namespace::G)) => "g",
|
||||||
|
Some(Operator::Object { around: false }) => "i",
|
||||||
|
Some(Operator::Object { around: true }) => "a",
|
||||||
Some(Operator::Change) => "c",
|
Some(Operator::Change) => "c",
|
||||||
Some(Operator::Delete) => "d",
|
Some(Operator::Delete) => "d",
|
||||||
Some(Operator::Yank) => "y",
|
Some(Operator::Yank) => "y",
|
||||||
|
|
||||||
None => "none",
|
None => "none",
|
||||||
}
|
}
|
||||||
.to_owned();
|
.to_owned();
|
||||||
|
|
103
crates/vim/src/test.rs
Normal file
103
crates/vim/src/test.rs
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
mod neovim_backed_binding_test_context;
|
||||||
|
mod neovim_backed_test_context;
|
||||||
|
mod neovim_connection;
|
||||||
|
mod vim_binding_test_context;
|
||||||
|
mod vim_test_context;
|
||||||
|
|
||||||
|
pub use neovim_backed_binding_test_context::*;
|
||||||
|
pub use neovim_backed_test_context::*;
|
||||||
|
pub use vim_binding_test_context::*;
|
||||||
|
pub use vim_test_context::*;
|
||||||
|
|
||||||
|
use indoc::indoc;
|
||||||
|
use search::BufferSearchBar;
|
||||||
|
|
||||||
|
use crate::state::Mode;
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = VimTestContext::new(cx, false).await;
|
||||||
|
cx.simulate_keystrokes(["h", "j", "k", "l"]);
|
||||||
|
cx.assert_editor_state("hjklˇ");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_neovim(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
|
||||||
|
cx.simulate_shared_keystroke("i").await;
|
||||||
|
cx.assert_state_matches().await;
|
||||||
|
cx.simulate_shared_keystrokes([
|
||||||
|
"shift-T", "e", "s", "t", " ", "t", "e", "s", "t", "escape", "0", "d", "w",
|
||||||
|
])
|
||||||
|
.await;
|
||||||
|
cx.assert_state_matches().await;
|
||||||
|
cx.assert_editor_state("ˇtest");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = VimTestContext::new(cx, true).await;
|
||||||
|
|
||||||
|
cx.simulate_keystroke("i");
|
||||||
|
assert_eq!(cx.mode(), Mode::Insert);
|
||||||
|
|
||||||
|
// Editor acts as though vim is disabled
|
||||||
|
cx.disable_vim();
|
||||||
|
cx.simulate_keystrokes(["h", "j", "k", "l"]);
|
||||||
|
cx.assert_editor_state("hjklˇ");
|
||||||
|
|
||||||
|
// Selections aren't changed if editor is blurred but vim-mode is still disabled.
|
||||||
|
cx.set_state("«hjklˇ»", Mode::Normal);
|
||||||
|
cx.assert_editor_state("«hjklˇ»");
|
||||||
|
cx.update_editor(|_, cx| cx.blur());
|
||||||
|
cx.assert_editor_state("«hjklˇ»");
|
||||||
|
cx.update_editor(|_, cx| cx.focus_self());
|
||||||
|
cx.assert_editor_state("«hjklˇ»");
|
||||||
|
|
||||||
|
// Enabling dynamically sets vim mode again and restores normal mode
|
||||||
|
cx.enable_vim();
|
||||||
|
assert_eq!(cx.mode(), Mode::Normal);
|
||||||
|
cx.simulate_keystrokes(["h", "h", "h", "l"]);
|
||||||
|
assert_eq!(cx.buffer_text(), "hjkl".to_owned());
|
||||||
|
cx.assert_editor_state("hˇjkl");
|
||||||
|
cx.simulate_keystrokes(["i", "T", "e", "s", "t"]);
|
||||||
|
cx.assert_editor_state("hTestˇjkl");
|
||||||
|
|
||||||
|
// Disabling and enabling resets to normal mode
|
||||||
|
assert_eq!(cx.mode(), Mode::Insert);
|
||||||
|
cx.disable_vim();
|
||||||
|
cx.enable_vim();
|
||||||
|
assert_eq!(cx.mode(), Mode::Normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_buffer_search(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = VimTestContext::new(cx, true).await;
|
||||||
|
|
||||||
|
cx.set_state(
|
||||||
|
indoc! {"
|
||||||
|
The quick brown
|
||||||
|
fox juˇmps over
|
||||||
|
the lazy dog"},
|
||||||
|
Mode::Normal,
|
||||||
|
);
|
||||||
|
cx.simulate_keystroke("/");
|
||||||
|
|
||||||
|
// We now use a weird insert mode with selection when jumping to a single line editor
|
||||||
|
assert_eq!(cx.mode(), Mode::Insert);
|
||||||
|
|
||||||
|
let search_bar = cx.workspace(|workspace, cx| {
|
||||||
|
workspace
|
||||||
|
.active_pane()
|
||||||
|
.read(cx)
|
||||||
|
.toolbar()
|
||||||
|
.read(cx)
|
||||||
|
.item_of_type::<BufferSearchBar>()
|
||||||
|
.expect("Buffer search bar should be deployed")
|
||||||
|
});
|
||||||
|
|
||||||
|
search_bar.read_with(cx.cx, |bar, cx| {
|
||||||
|
assert_eq!(bar.query_editor.read(cx).text(cx), "jumps");
|
||||||
|
})
|
||||||
|
}
|
80
crates/vim/src/test/neovim_backed_binding_test_context.rs
Normal file
80
crates/vim/src/test/neovim_backed_binding_test_context.rs
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
|
use gpui::ContextHandle;
|
||||||
|
|
||||||
|
use crate::state::Mode;
|
||||||
|
|
||||||
|
use super::NeovimBackedTestContext;
|
||||||
|
|
||||||
|
pub struct NeovimBackedBindingTestContext<'a, const COUNT: usize> {
|
||||||
|
cx: NeovimBackedTestContext<'a>,
|
||||||
|
keystrokes_under_test: [&'static str; COUNT],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, const COUNT: usize> NeovimBackedBindingTestContext<'a, COUNT> {
|
||||||
|
pub fn new(
|
||||||
|
keystrokes_under_test: [&'static str; COUNT],
|
||||||
|
cx: NeovimBackedTestContext<'a>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
cx,
|
||||||
|
keystrokes_under_test,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn consume(self) -> NeovimBackedTestContext<'a> {
|
||||||
|
self.cx
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn binding<const NEW_COUNT: usize>(
|
||||||
|
self,
|
||||||
|
keystrokes: [&'static str; NEW_COUNT],
|
||||||
|
) -> NeovimBackedBindingTestContext<'a, NEW_COUNT> {
|
||||||
|
self.consume().binding(keystrokes)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn assert(
|
||||||
|
&mut self,
|
||||||
|
marked_positions: &str,
|
||||||
|
) -> Option<(ContextHandle, ContextHandle)> {
|
||||||
|
self.cx
|
||||||
|
.assert_binding_matches(self.keystrokes_under_test, marked_positions)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assert_manual(
|
||||||
|
&mut self,
|
||||||
|
initial_state: &str,
|
||||||
|
mode_before: Mode,
|
||||||
|
state_after: &str,
|
||||||
|
mode_after: Mode,
|
||||||
|
) {
|
||||||
|
self.cx.assert_binding(
|
||||||
|
self.keystrokes_under_test,
|
||||||
|
initial_state,
|
||||||
|
mode_before,
|
||||||
|
state_after,
|
||||||
|
mode_after,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn assert_all(&mut self, marked_positions: &str) {
|
||||||
|
self.cx
|
||||||
|
.assert_binding_matches_all(self.keystrokes_under_test, marked_positions)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, const COUNT: usize> Deref for NeovimBackedBindingTestContext<'a, COUNT> {
|
||||||
|
type Target = NeovimBackedTestContext<'a>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.cx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, const COUNT: usize> DerefMut for NeovimBackedBindingTestContext<'a, COUNT> {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.cx
|
||||||
|
}
|
||||||
|
}
|
158
crates/vim/src/test/neovim_backed_test_context.rs
Normal file
158
crates/vim/src/test/neovim_backed_test_context.rs
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
|
use collections::{HashMap, HashSet};
|
||||||
|
use gpui::ContextHandle;
|
||||||
|
use language::OffsetRangeExt;
|
||||||
|
use util::test::marked_text_offsets;
|
||||||
|
|
||||||
|
use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
|
||||||
|
use crate::state::Mode;
|
||||||
|
|
||||||
|
pub struct NeovimBackedTestContext<'a> {
|
||||||
|
cx: VimTestContext<'a>,
|
||||||
|
// Lookup for exempted assertions. Keyed by the insertion text, and with a value indicating which
|
||||||
|
// bindings are exempted. If None, all bindings are ignored for that insertion text.
|
||||||
|
exemptions: HashMap<String, Option<HashSet<String>>>,
|
||||||
|
neovim: NeovimConnection,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> NeovimBackedTestContext<'a> {
|
||||||
|
pub async fn new(cx: &'a mut gpui::TestAppContext) -> NeovimBackedTestContext<'a> {
|
||||||
|
let function_name = cx.function_name.clone();
|
||||||
|
let cx = VimTestContext::new(cx, true).await;
|
||||||
|
Self {
|
||||||
|
cx,
|
||||||
|
exemptions: Default::default(),
|
||||||
|
neovim: NeovimConnection::new(function_name).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_initial_state_exemption(&mut self, initial_state: &str) {
|
||||||
|
let initial_state = initial_state.to_string();
|
||||||
|
// None represents all keybindings being exempted for that initial state
|
||||||
|
self.exemptions.insert(initial_state, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn simulate_shared_keystroke(&mut self, keystroke_text: &str) -> ContextHandle {
|
||||||
|
self.neovim.send_keystroke(keystroke_text).await;
|
||||||
|
self.simulate_keystroke(keystroke_text)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn simulate_shared_keystrokes<const COUNT: usize>(
|
||||||
|
&mut self,
|
||||||
|
keystroke_texts: [&str; COUNT],
|
||||||
|
) -> ContextHandle {
|
||||||
|
for keystroke_text in keystroke_texts.into_iter() {
|
||||||
|
self.neovim.send_keystroke(keystroke_text).await;
|
||||||
|
}
|
||||||
|
self.simulate_keystrokes(keystroke_texts)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_shared_state(&mut self, marked_text: &str) -> ContextHandle {
|
||||||
|
let context_handle = self.set_state(marked_text, Mode::Normal);
|
||||||
|
|
||||||
|
let selection = self.editor(|editor, cx| editor.selections.newest::<language::Point>(cx));
|
||||||
|
let text = self.buffer_text();
|
||||||
|
self.neovim.set_state(selection, &text).await;
|
||||||
|
|
||||||
|
context_handle
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn assert_state_matches(&mut self) {
|
||||||
|
assert_eq!(
|
||||||
|
self.neovim.text().await,
|
||||||
|
self.buffer_text(),
|
||||||
|
"{}",
|
||||||
|
self.assertion_context()
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut neovim_selection = self.neovim.selection().await;
|
||||||
|
// Zed selections adjust themselves to make the end point visually make sense
|
||||||
|
if neovim_selection.start > neovim_selection.end {
|
||||||
|
neovim_selection.start.column += 1;
|
||||||
|
}
|
||||||
|
let neovim_selection = neovim_selection.to_offset(&self.buffer_snapshot());
|
||||||
|
self.assert_editor_selections(vec![neovim_selection]);
|
||||||
|
|
||||||
|
if let Some(neovim_mode) = self.neovim.mode().await {
|
||||||
|
assert_eq!(neovim_mode, self.mode(), "{}", self.assertion_context(),);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn assert_binding_matches<const COUNT: usize>(
|
||||||
|
&mut self,
|
||||||
|
keystrokes: [&str; COUNT],
|
||||||
|
initial_state: &str,
|
||||||
|
) -> Option<(ContextHandle, ContextHandle)> {
|
||||||
|
if let Some(possible_exempted_keystrokes) = self.exemptions.get(initial_state) {
|
||||||
|
match possible_exempted_keystrokes {
|
||||||
|
Some(exempted_keystrokes) => {
|
||||||
|
if exempted_keystrokes.contains(&format!("{keystrokes:?}")) {
|
||||||
|
// This keystroke was exempted for this insertion text
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// All keystrokes for this insertion text are exempted
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _state_context = self.set_shared_state(initial_state).await;
|
||||||
|
let _keystroke_context = self.simulate_shared_keystrokes(keystrokes).await;
|
||||||
|
self.assert_state_matches().await;
|
||||||
|
Some((_state_context, _keystroke_context))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn assert_binding_matches_all<const COUNT: usize>(
|
||||||
|
&mut self,
|
||||||
|
keystrokes: [&str; COUNT],
|
||||||
|
marked_positions: &str,
|
||||||
|
) {
|
||||||
|
let (unmarked_text, cursor_offsets) = marked_text_offsets(marked_positions);
|
||||||
|
|
||||||
|
for cursor_offset in cursor_offsets.iter() {
|
||||||
|
let mut marked_text = unmarked_text.clone();
|
||||||
|
marked_text.insert(*cursor_offset, 'ˇ');
|
||||||
|
|
||||||
|
self.assert_binding_matches(keystrokes, &marked_text).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn binding<const COUNT: usize>(
|
||||||
|
self,
|
||||||
|
keystrokes: [&'static str; COUNT],
|
||||||
|
) -> NeovimBackedBindingTestContext<'a, COUNT> {
|
||||||
|
NeovimBackedBindingTestContext::new(keystrokes, self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Deref for NeovimBackedTestContext<'a> {
|
||||||
|
type Target = VimTestContext<'a>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.cx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> DerefMut for NeovimBackedTestContext<'a> {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.cx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use gpui::TestAppContext;
|
||||||
|
|
||||||
|
use crate::test::NeovimBackedTestContext;
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn neovim_backed_test_context_works(cx: &mut TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
cx.assert_state_matches().await;
|
||||||
|
cx.set_shared_state("This is a tesˇt").await;
|
||||||
|
cx.assert_state_matches().await;
|
||||||
|
}
|
||||||
|
}
|
383
crates/vim/src/test/neovim_connection.rs
Normal file
383
crates/vim/src/test/neovim_connection.rs
Normal file
|
@ -0,0 +1,383 @@
|
||||||
|
#[cfg(feature = "neovim")]
|
||||||
|
use std::ops::{Deref, DerefMut};
|
||||||
|
use std::{ops::Range, path::PathBuf};
|
||||||
|
|
||||||
|
#[cfg(feature = "neovim")]
|
||||||
|
use async_compat::Compat;
|
||||||
|
#[cfg(feature = "neovim")]
|
||||||
|
use async_trait::async_trait;
|
||||||
|
#[cfg(feature = "neovim")]
|
||||||
|
use gpui::keymap::Keystroke;
|
||||||
|
use language::{Point, Selection};
|
||||||
|
#[cfg(feature = "neovim")]
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
#[cfg(feature = "neovim")]
|
||||||
|
use nvim_rs::{
|
||||||
|
create::tokio::new_child_cmd, error::LoopError, Handler, Neovim, UiAttachOptions, Value,
|
||||||
|
};
|
||||||
|
#[cfg(feature = "neovim")]
|
||||||
|
use parking_lot::ReentrantMutex;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
#[cfg(feature = "neovim")]
|
||||||
|
use tokio::{
|
||||||
|
process::{Child, ChildStdin, Command},
|
||||||
|
task::JoinHandle,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::state::Mode;
|
||||||
|
use collections::VecDeque;
|
||||||
|
|
||||||
|
// Neovim doesn't like to be started simultaneously from multiple threads. We use thsi lock
|
||||||
|
// to ensure we are only constructing one neovim connection at a time.
|
||||||
|
#[cfg(feature = "neovim")]
|
||||||
|
lazy_static! {
|
||||||
|
static ref NEOVIM_LOCK: ReentrantMutex<()> = ReentrantMutex::new(());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub enum NeovimData {
|
||||||
|
Text(String),
|
||||||
|
Selection { start: (u32, u32), end: (u32, u32) },
|
||||||
|
Mode(Option<Mode>),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NeovimConnection {
|
||||||
|
data: VecDeque<NeovimData>,
|
||||||
|
#[cfg(feature = "neovim")]
|
||||||
|
test_case_id: String,
|
||||||
|
#[cfg(feature = "neovim")]
|
||||||
|
nvim: Neovim<nvim_rs::compat::tokio::Compat<ChildStdin>>,
|
||||||
|
#[cfg(feature = "neovim")]
|
||||||
|
_join_handle: JoinHandle<Result<(), Box<LoopError>>>,
|
||||||
|
#[cfg(feature = "neovim")]
|
||||||
|
_child: Child,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NeovimConnection {
|
||||||
|
pub async fn new(test_case_id: String) -> Self {
|
||||||
|
#[cfg(feature = "neovim")]
|
||||||
|
let handler = NvimHandler {};
|
||||||
|
#[cfg(feature = "neovim")]
|
||||||
|
let (nvim, join_handle, child) = Compat::new(async {
|
||||||
|
// Ensure we don't create neovim connections in parallel
|
||||||
|
let _lock = NEOVIM_LOCK.lock();
|
||||||
|
let (nvim, join_handle, child) = new_child_cmd(
|
||||||
|
&mut Command::new("nvim").arg("--embed").arg("--clean"),
|
||||||
|
handler,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Could not connect to neovim process");
|
||||||
|
|
||||||
|
nvim.ui_attach(100, 100, &UiAttachOptions::default())
|
||||||
|
.await
|
||||||
|
.expect("Could not attach to ui");
|
||||||
|
|
||||||
|
// Makes system act a little more like zed in terms of indentation
|
||||||
|
nvim.set_option("smartindent", nvim_rs::Value::Boolean(true))
|
||||||
|
.await
|
||||||
|
.expect("Could not set smartindent on startup");
|
||||||
|
|
||||||
|
(nvim, join_handle, child)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
Self {
|
||||||
|
#[cfg(feature = "neovim")]
|
||||||
|
data: Default::default(),
|
||||||
|
#[cfg(not(feature = "neovim"))]
|
||||||
|
data: Self::read_test_data(&test_case_id),
|
||||||
|
#[cfg(feature = "neovim")]
|
||||||
|
test_case_id,
|
||||||
|
#[cfg(feature = "neovim")]
|
||||||
|
nvim,
|
||||||
|
#[cfg(feature = "neovim")]
|
||||||
|
_join_handle: join_handle,
|
||||||
|
#[cfg(feature = "neovim")]
|
||||||
|
_child: child,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sends a keystroke to the neovim process.
|
||||||
|
#[cfg(feature = "neovim")]
|
||||||
|
pub async fn send_keystroke(&mut self, keystroke_text: &str) {
|
||||||
|
let keystroke = Keystroke::parse(keystroke_text).unwrap();
|
||||||
|
let special = keystroke.shift
|
||||||
|
|| keystroke.ctrl
|
||||||
|
|| keystroke.alt
|
||||||
|
|| keystroke.cmd
|
||||||
|
|| keystroke.key.len() > 1;
|
||||||
|
let start = if special { "<" } else { "" };
|
||||||
|
let shift = if keystroke.shift { "S-" } else { "" };
|
||||||
|
let ctrl = if keystroke.ctrl { "C-" } else { "" };
|
||||||
|
let alt = if keystroke.alt { "M-" } else { "" };
|
||||||
|
let cmd = if keystroke.cmd { "D-" } else { "" };
|
||||||
|
let end = if special { ">" } else { "" };
|
||||||
|
|
||||||
|
let key = format!("{start}{shift}{ctrl}{alt}{cmd}{}{end}", keystroke.key);
|
||||||
|
|
||||||
|
self.nvim
|
||||||
|
.input(&key)
|
||||||
|
.await
|
||||||
|
.expect("Could not input keystroke");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not running with a live neovim connection, this is a no-op
|
||||||
|
#[cfg(not(feature = "neovim"))]
|
||||||
|
pub async fn send_keystroke(&mut self, _keystroke_text: &str) {}
|
||||||
|
|
||||||
|
#[cfg(feature = "neovim")]
|
||||||
|
pub async fn set_state(&mut self, selection: Selection<Point>, text: &str) {
|
||||||
|
let nvim_buffer = self
|
||||||
|
.nvim
|
||||||
|
.get_current_buf()
|
||||||
|
.await
|
||||||
|
.expect("Could not get neovim buffer");
|
||||||
|
let lines = text
|
||||||
|
.split('\n')
|
||||||
|
.map(|line| line.to_string())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
nvim_buffer
|
||||||
|
.set_lines(0, -1, false, lines)
|
||||||
|
.await
|
||||||
|
.expect("Could not set nvim buffer text");
|
||||||
|
|
||||||
|
self.nvim
|
||||||
|
.input("<escape>")
|
||||||
|
.await
|
||||||
|
.expect("Could not send escape to nvim");
|
||||||
|
self.nvim
|
||||||
|
.input("<escape>")
|
||||||
|
.await
|
||||||
|
.expect("Could not send escape to nvim");
|
||||||
|
|
||||||
|
let nvim_window = self
|
||||||
|
.nvim
|
||||||
|
.get_current_win()
|
||||||
|
.await
|
||||||
|
.expect("Could not get neovim window");
|
||||||
|
|
||||||
|
if !selection.is_empty() {
|
||||||
|
panic!("Setting neovim state with non empty selection not yet supported");
|
||||||
|
}
|
||||||
|
let cursor = selection.head();
|
||||||
|
nvim_window
|
||||||
|
.set_cursor((cursor.row as i64 + 1, cursor.column as i64))
|
||||||
|
.await
|
||||||
|
.expect("Could not set nvim cursor position");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "neovim"))]
|
||||||
|
pub async fn set_state(&mut self, _selection: Selection<Point>, _text: &str) {}
|
||||||
|
|
||||||
|
#[cfg(feature = "neovim")]
|
||||||
|
pub async fn text(&mut self) -> String {
|
||||||
|
let nvim_buffer = self
|
||||||
|
.nvim
|
||||||
|
.get_current_buf()
|
||||||
|
.await
|
||||||
|
.expect("Could not get neovim buffer");
|
||||||
|
let text = nvim_buffer
|
||||||
|
.get_lines(0, -1, false)
|
||||||
|
.await
|
||||||
|
.expect("Could not get buffer text")
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
self.data.push_back(NeovimData::Text(text.clone()));
|
||||||
|
|
||||||
|
text
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "neovim"))]
|
||||||
|
pub async fn text(&mut self) -> String {
|
||||||
|
if let Some(NeovimData::Text(text)) = self.data.pop_front() {
|
||||||
|
text
|
||||||
|
} else {
|
||||||
|
panic!("Invalid test data. Is test deterministic? Try running with '--features neovim' to regenerate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "neovim")]
|
||||||
|
pub async fn selection(&mut self) -> Range<Point> {
|
||||||
|
let cursor_row: u32 = self
|
||||||
|
.nvim
|
||||||
|
.command_output("echo line('.')")
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.parse::<u32>()
|
||||||
|
.unwrap()
|
||||||
|
- 1; // Neovim rows start at 1
|
||||||
|
let cursor_col: u32 = self
|
||||||
|
.nvim
|
||||||
|
.command_output("echo col('.')")
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.parse::<u32>()
|
||||||
|
.unwrap()
|
||||||
|
- 1; // Neovim columns start at 1
|
||||||
|
|
||||||
|
let (start, end) = if let Some(Mode::Visual { .. }) = self.mode().await {
|
||||||
|
self.nvim
|
||||||
|
.input("<escape>")
|
||||||
|
.await
|
||||||
|
.expect("Could not exit visual mode");
|
||||||
|
let nvim_buffer = self
|
||||||
|
.nvim
|
||||||
|
.get_current_buf()
|
||||||
|
.await
|
||||||
|
.expect("Could not get neovim buffer");
|
||||||
|
let (start_row, start_col) = nvim_buffer
|
||||||
|
.get_mark("<")
|
||||||
|
.await
|
||||||
|
.expect("Could not get selection start");
|
||||||
|
let (end_row, end_col) = nvim_buffer
|
||||||
|
.get_mark(">")
|
||||||
|
.await
|
||||||
|
.expect("Could not get selection end");
|
||||||
|
self.nvim
|
||||||
|
.input("gv")
|
||||||
|
.await
|
||||||
|
.expect("Could not reselect visual selection");
|
||||||
|
|
||||||
|
if cursor_row == start_row as u32 - 1 && cursor_col == start_col as u32 {
|
||||||
|
(
|
||||||
|
(end_row as u32 - 1, end_col as u32),
|
||||||
|
(start_row as u32 - 1, start_col as u32),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
(start_row as u32 - 1, start_col as u32),
|
||||||
|
(end_row as u32 - 1, end_col as u32),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
((cursor_row, cursor_col), (cursor_row, cursor_col))
|
||||||
|
};
|
||||||
|
|
||||||
|
self.data.push_back(NeovimData::Selection { start, end });
|
||||||
|
|
||||||
|
Point::new(start.0, start.1)..Point::new(end.0, end.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "neovim"))]
|
||||||
|
pub async fn selection(&mut self) -> Range<Point> {
|
||||||
|
// Selection code fetches the mode. This emulates that.
|
||||||
|
let _mode = self.mode().await;
|
||||||
|
if let Some(NeovimData::Selection { start, end }) = self.data.pop_front() {
|
||||||
|
Point::new(start.0, start.1)..Point::new(end.0, end.1)
|
||||||
|
} else {
|
||||||
|
panic!("Invalid test data. Is test deterministic? Try running with '--features neovim' to regenerate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "neovim")]
|
||||||
|
pub async fn mode(&mut self) -> Option<Mode> {
|
||||||
|
let nvim_mode_text = self
|
||||||
|
.nvim
|
||||||
|
.get_mode()
|
||||||
|
.await
|
||||||
|
.expect("Could not get mode")
|
||||||
|
.into_iter()
|
||||||
|
.find_map(|(key, value)| {
|
||||||
|
if key.as_str() == Some("mode") {
|
||||||
|
Some(value.as_str().unwrap().to_owned())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.expect("Could not find mode value");
|
||||||
|
|
||||||
|
let mode = match nvim_mode_text.as_ref() {
|
||||||
|
"i" => Some(Mode::Insert),
|
||||||
|
"n" => Some(Mode::Normal),
|
||||||
|
"v" => Some(Mode::Visual { line: false }),
|
||||||
|
"V" => Some(Mode::Visual { line: true }),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.data.push_back(NeovimData::Mode(mode.clone()));
|
||||||
|
|
||||||
|
mode
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "neovim"))]
|
||||||
|
pub async fn mode(&mut self) -> Option<Mode> {
|
||||||
|
if let Some(NeovimData::Mode(mode)) = self.data.pop_front() {
|
||||||
|
mode
|
||||||
|
} else {
|
||||||
|
panic!("Invalid test data. Is test deterministic? Try running with '--features neovim' to regenerate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_data_path(test_case_id: &str) -> PathBuf {
|
||||||
|
let mut data_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
data_path.push("test_data");
|
||||||
|
data_path.push(format!("{}.json", test_case_id));
|
||||||
|
data_path
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "neovim"))]
|
||||||
|
fn read_test_data(test_case_id: &str) -> VecDeque<NeovimData> {
|
||||||
|
let path = Self::test_data_path(test_case_id);
|
||||||
|
let json = std::fs::read_to_string(path).expect(
|
||||||
|
"Could not read test data. Is it generated? Try running test with '--features neovim'",
|
||||||
|
);
|
||||||
|
|
||||||
|
serde_json::from_str(&json)
|
||||||
|
.expect("Test data corrupted. Try regenerating it with '--features neovim'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "neovim")]
|
||||||
|
impl Deref for NeovimConnection {
|
||||||
|
type Target = Neovim<nvim_rs::compat::tokio::Compat<ChildStdin>>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.nvim
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "neovim")]
|
||||||
|
impl DerefMut for NeovimConnection {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.nvim
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "neovim")]
|
||||||
|
impl Drop for NeovimConnection {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let path = Self::test_data_path(&self.test_case_id);
|
||||||
|
std::fs::create_dir_all(path.parent().unwrap())
|
||||||
|
.expect("Could not create test data directory");
|
||||||
|
let json = serde_json::to_string(&self.data).expect("Could not serialize test data");
|
||||||
|
std::fs::write(path, json).expect("Could not write out test data");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "neovim")]
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct NvimHandler {}
|
||||||
|
|
||||||
|
#[cfg(feature = "neovim")]
|
||||||
|
#[async_trait]
|
||||||
|
impl Handler for NvimHandler {
|
||||||
|
type Writer = nvim_rs::compat::tokio::Compat<ChildStdin>;
|
||||||
|
|
||||||
|
async fn handle_request(
|
||||||
|
&self,
|
||||||
|
_event_name: String,
|
||||||
|
_arguments: Vec<Value>,
|
||||||
|
_neovim: Neovim<Self::Writer>,
|
||||||
|
) -> Result<Value, Value> {
|
||||||
|
unimplemented!();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_notify(
|
||||||
|
&self,
|
||||||
|
_event_name: String,
|
||||||
|
_arguments: Vec<Value>,
|
||||||
|
_neovim: Neovim<Self::Writer>,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
69
crates/vim/src/test/vim_binding_test_context.rs
Normal file
69
crates/vim/src/test/vim_binding_test_context.rs
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
use super::VimTestContext;
|
||||||
|
|
||||||
|
pub struct VimBindingTestContext<'a, const COUNT: usize> {
|
||||||
|
cx: VimTestContext<'a>,
|
||||||
|
keystrokes_under_test: [&'static str; COUNT],
|
||||||
|
mode_before: Mode,
|
||||||
|
mode_after: Mode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, const COUNT: usize> VimBindingTestContext<'a, COUNT> {
|
||||||
|
pub fn new(
|
||||||
|
keystrokes_under_test: [&'static str; COUNT],
|
||||||
|
mode_before: Mode,
|
||||||
|
mode_after: Mode,
|
||||||
|
cx: VimTestContext<'a>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
cx,
|
||||||
|
keystrokes_under_test,
|
||||||
|
mode_before,
|
||||||
|
mode_after,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn binding<const NEW_COUNT: usize>(
|
||||||
|
self,
|
||||||
|
keystrokes_under_test: [&'static str; NEW_COUNT],
|
||||||
|
) -> VimBindingTestContext<'a, NEW_COUNT> {
|
||||||
|
VimBindingTestContext {
|
||||||
|
keystrokes_under_test,
|
||||||
|
cx: self.cx,
|
||||||
|
mode_before: self.mode_before,
|
||||||
|
mode_after: self.mode_after,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mode_after(mut self, mode_after: Mode) -> Self {
|
||||||
|
self.mode_after = mode_after;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assert(&mut self, initial_state: &str, state_after: &str) {
|
||||||
|
self.cx.assert_binding(
|
||||||
|
self.keystrokes_under_test,
|
||||||
|
initial_state,
|
||||||
|
self.mode_before,
|
||||||
|
state_after,
|
||||||
|
self.mode_after,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, const COUNT: usize> Deref for VimBindingTestContext<'a, COUNT> {
|
||||||
|
type Target = VimTestContext<'a>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.cx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, const COUNT: usize> DerefMut for VimBindingTestContext<'a, COUNT> {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.cx
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +1,15 @@
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
use editor::test::EditorTestContext;
|
use editor::test::editor_test_context::EditorTestContext;
|
||||||
use gpui::{json::json, AppContext, ViewHandle};
|
use gpui::{json::json, AppContext, ContextHandle, ViewHandle};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use search::{BufferSearchBar, ProjectSearchBar};
|
use search::{BufferSearchBar, ProjectSearchBar};
|
||||||
use workspace::{pane, AppState, WorkspaceHandle};
|
use workspace::{pane, AppState, WorkspaceHandle};
|
||||||
|
|
||||||
use crate::{state::Operator, *};
|
use crate::{state::Operator, *};
|
||||||
|
|
||||||
|
use super::VimBindingTestContext;
|
||||||
|
|
||||||
pub struct VimTestContext<'a> {
|
pub struct VimTestContext<'a> {
|
||||||
cx: EditorTestContext<'a>,
|
cx: EditorTestContext<'a>,
|
||||||
workspace: ViewHandle<Workspace>,
|
workspace: ViewHandle<Workspace>,
|
||||||
|
@ -117,18 +119,18 @@ impl<'a> VimTestContext<'a> {
|
||||||
.read(|cx| cx.global::<Vim>().state.operator_stack.last().copied())
|
.read(|cx| cx.global::<Vim>().state.operator_stack.last().copied())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_state(&mut self, text: &str, mode: Mode) {
|
pub fn set_state(&mut self, text: &str, mode: Mode) -> ContextHandle {
|
||||||
self.cx.update(|cx| {
|
self.cx.update(|cx| {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
vim.switch_mode(mode, false, cx);
|
vim.switch_mode(mode, false, cx);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
self.cx.set_state(text);
|
self.cx.set_state(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn assert_state(&mut self, text: &str, mode: Mode) {
|
pub fn assert_state(&mut self, text: &str, mode: Mode) {
|
||||||
self.assert_editor_state(text);
|
self.assert_editor_state(text);
|
||||||
assert_eq!(self.mode(), mode);
|
assert_eq!(self.mode(), mode, "{}", self.assertion_context());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn assert_binding<const COUNT: usize>(
|
pub fn assert_binding<const COUNT: usize>(
|
||||||
|
@ -142,8 +144,8 @@ impl<'a> VimTestContext<'a> {
|
||||||
self.set_state(initial_state, initial_mode);
|
self.set_state(initial_state, initial_mode);
|
||||||
self.cx.simulate_keystrokes(keystrokes);
|
self.cx.simulate_keystrokes(keystrokes);
|
||||||
self.cx.assert_editor_state(state_after);
|
self.cx.assert_editor_state(state_after);
|
||||||
assert_eq!(self.mode(), mode_after);
|
assert_eq!(self.mode(), mode_after, "{}", self.assertion_context());
|
||||||
assert_eq!(self.active_operator(), None);
|
assert_eq!(self.active_operator(), None, "{}", self.assertion_context());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn binding<const COUNT: usize>(
|
pub fn binding<const COUNT: usize>(
|
||||||
|
@ -168,67 +170,3 @@ impl<'a> DerefMut for VimTestContext<'a> {
|
||||||
&mut self.cx
|
&mut self.cx
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct VimBindingTestContext<'a, const COUNT: usize> {
|
|
||||||
cx: VimTestContext<'a>,
|
|
||||||
keystrokes_under_test: [&'static str; COUNT],
|
|
||||||
mode_before: Mode,
|
|
||||||
mode_after: Mode,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, const COUNT: usize> VimBindingTestContext<'a, COUNT> {
|
|
||||||
pub fn new(
|
|
||||||
keystrokes_under_test: [&'static str; COUNT],
|
|
||||||
mode_before: Mode,
|
|
||||||
mode_after: Mode,
|
|
||||||
cx: VimTestContext<'a>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
cx,
|
|
||||||
keystrokes_under_test,
|
|
||||||
mode_before,
|
|
||||||
mode_after,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn binding<const NEW_COUNT: usize>(
|
|
||||||
self,
|
|
||||||
keystrokes_under_test: [&'static str; NEW_COUNT],
|
|
||||||
) -> VimBindingTestContext<'a, NEW_COUNT> {
|
|
||||||
VimBindingTestContext {
|
|
||||||
keystrokes_under_test,
|
|
||||||
cx: self.cx,
|
|
||||||
mode_before: self.mode_before,
|
|
||||||
mode_after: self.mode_after,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mode_after(mut self, mode_after: Mode) -> Self {
|
|
||||||
self.mode_after = mode_after;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn assert(&mut self, initial_state: &str, state_after: &str) {
|
|
||||||
self.cx.assert_binding(
|
|
||||||
self.keystrokes_under_test,
|
|
||||||
initial_state,
|
|
||||||
self.mode_before,
|
|
||||||
state_after,
|
|
||||||
self.mode_after,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, const COUNT: usize> Deref for VimBindingTestContext<'a, COUNT> {
|
|
||||||
type Target = VimTestContext<'a>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.cx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, const COUNT: usize> DerefMut for VimBindingTestContext<'a, COUNT> {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.cx
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +1,11 @@
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod vim_test_context;
|
mod test;
|
||||||
|
|
||||||
mod editor_events;
|
mod editor_events;
|
||||||
mod insert;
|
mod insert;
|
||||||
mod motion;
|
mod motion;
|
||||||
mod normal;
|
mod normal;
|
||||||
|
mod object;
|
||||||
mod state;
|
mod state;
|
||||||
mod utils;
|
mod utils;
|
||||||
mod visual;
|
mod visual;
|
||||||
|
@ -25,13 +26,17 @@ pub struct SwitchMode(pub Mode);
|
||||||
#[derive(Clone, Deserialize, PartialEq)]
|
#[derive(Clone, Deserialize, PartialEq)]
|
||||||
pub struct PushOperator(pub Operator);
|
pub struct PushOperator(pub Operator);
|
||||||
|
|
||||||
impl_actions!(vim, [SwitchMode, PushOperator]);
|
#[derive(Clone, Deserialize, PartialEq)]
|
||||||
|
struct Number(u8);
|
||||||
|
|
||||||
|
impl_actions!(vim, [Number, SwitchMode, PushOperator]);
|
||||||
|
|
||||||
pub fn init(cx: &mut MutableAppContext) {
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
editor_events::init(cx);
|
editor_events::init(cx);
|
||||||
normal::init(cx);
|
normal::init(cx);
|
||||||
visual::init(cx);
|
visual::init(cx);
|
||||||
insert::init(cx);
|
insert::init(cx);
|
||||||
|
object::init(cx);
|
||||||
motion::init(cx);
|
motion::init(cx);
|
||||||
|
|
||||||
// Vim Actions
|
// Vim Actions
|
||||||
|
@ -43,6 +48,9 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||||
Vim::update(cx, |vim, cx| vim.push_operator(operator, cx))
|
Vim::update(cx, |vim, cx| vim.push_operator(operator, cx))
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
cx.add_action(|_: &mut Workspace, n: &Number, cx: _| {
|
||||||
|
Vim::update(cx, |vim, cx| vim.push_number(n, cx));
|
||||||
|
});
|
||||||
|
|
||||||
// Editor Actions
|
// Editor Actions
|
||||||
cx.add_action(|_: &mut Editor, _: &Cancel, cx| {
|
cx.add_action(|_: &mut Editor, _: &Cancel, cx| {
|
||||||
|
@ -143,12 +151,31 @@ impl Vim {
|
||||||
self.sync_vim_settings(cx);
|
self.sync_vim_settings(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn push_number(&mut self, Number(number): &Number, cx: &mut MutableAppContext) {
|
||||||
|
if let Some(Operator::Number(current_number)) = self.active_operator() {
|
||||||
|
self.pop_operator(cx);
|
||||||
|
self.push_operator(Operator::Number(current_number * 10 + *number as usize), cx);
|
||||||
|
} else {
|
||||||
|
self.push_operator(Operator::Number(*number as usize), cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn pop_operator(&mut self, cx: &mut MutableAppContext) -> Operator {
|
fn pop_operator(&mut self, cx: &mut MutableAppContext) -> Operator {
|
||||||
let popped_operator = self.state.operator_stack.pop().expect("Operator popped when no operator was on the stack. This likely means there is an invalid keymap config");
|
let popped_operator = self.state.operator_stack.pop()
|
||||||
|
.expect("Operator popped when no operator was on the stack. This likely means there is an invalid keymap config");
|
||||||
self.sync_vim_settings(cx);
|
self.sync_vim_settings(cx);
|
||||||
popped_operator
|
popped_operator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn pop_number_operator(&mut self, cx: &mut MutableAppContext) -> usize {
|
||||||
|
let mut times = 1;
|
||||||
|
if let Some(Operator::Number(number)) = self.active_operator() {
|
||||||
|
times = number;
|
||||||
|
self.pop_operator(cx);
|
||||||
|
}
|
||||||
|
times
|
||||||
|
}
|
||||||
|
|
||||||
fn clear_operator(&mut self, cx: &mut MutableAppContext) {
|
fn clear_operator(&mut self, cx: &mut MutableAppContext) {
|
||||||
self.state.operator_stack.clear();
|
self.state.operator_stack.clear();
|
||||||
self.sync_vim_settings(cx);
|
self.sync_vim_settings(cx);
|
||||||
|
@ -204,85 +231,3 @@ impl Vim {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use indoc::indoc;
|
|
||||||
use search::BufferSearchBar;
|
|
||||||
|
|
||||||
use crate::{state::Mode, vim_test_context::VimTestContext};
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
|
|
||||||
let mut cx = VimTestContext::new(cx, false).await;
|
|
||||||
cx.simulate_keystrokes(["h", "j", "k", "l"]);
|
|
||||||
cx.assert_editor_state("hjklˇ");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) {
|
|
||||||
let mut cx = VimTestContext::new(cx, true).await;
|
|
||||||
|
|
||||||
cx.simulate_keystroke("i");
|
|
||||||
assert_eq!(cx.mode(), Mode::Insert);
|
|
||||||
|
|
||||||
// Editor acts as though vim is disabled
|
|
||||||
cx.disable_vim();
|
|
||||||
cx.simulate_keystrokes(["h", "j", "k", "l"]);
|
|
||||||
cx.assert_editor_state("hjklˇ");
|
|
||||||
|
|
||||||
// Selections aren't changed if editor is blurred but vim-mode is still disabled.
|
|
||||||
cx.set_state("«hjklˇ»", Mode::Normal);
|
|
||||||
cx.assert_editor_state("«hjklˇ»");
|
|
||||||
cx.update_editor(|_, cx| cx.blur());
|
|
||||||
cx.assert_editor_state("«hjklˇ»");
|
|
||||||
cx.update_editor(|_, cx| cx.focus_self());
|
|
||||||
cx.assert_editor_state("«hjklˇ»");
|
|
||||||
|
|
||||||
// Enabling dynamically sets vim mode again and restores normal mode
|
|
||||||
cx.enable_vim();
|
|
||||||
assert_eq!(cx.mode(), Mode::Normal);
|
|
||||||
cx.simulate_keystrokes(["h", "h", "h", "l"]);
|
|
||||||
assert_eq!(cx.buffer_text(), "hjkl".to_owned());
|
|
||||||
cx.assert_editor_state("hˇjkl");
|
|
||||||
cx.simulate_keystrokes(["i", "T", "e", "s", "t"]);
|
|
||||||
cx.assert_editor_state("hTestˇjkl");
|
|
||||||
|
|
||||||
// Disabling and enabling resets to normal mode
|
|
||||||
assert_eq!(cx.mode(), Mode::Insert);
|
|
||||||
cx.disable_vim();
|
|
||||||
cx.enable_vim();
|
|
||||||
assert_eq!(cx.mode(), Mode::Normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_buffer_search(cx: &mut gpui::TestAppContext) {
|
|
||||||
let mut cx = VimTestContext::new(cx, true).await;
|
|
||||||
|
|
||||||
cx.set_state(
|
|
||||||
indoc! {"
|
|
||||||
The quick brown
|
|
||||||
fox juˇmps over
|
|
||||||
the lazy dog"},
|
|
||||||
Mode::Normal,
|
|
||||||
);
|
|
||||||
cx.simulate_keystroke("/");
|
|
||||||
|
|
||||||
// We now use a weird insert mode with selection when jumping to a single line editor
|
|
||||||
assert_eq!(cx.mode(), Mode::Insert);
|
|
||||||
|
|
||||||
let search_bar = cx.workspace(|workspace, cx| {
|
|
||||||
workspace
|
|
||||||
.active_pane()
|
|
||||||
.read(cx)
|
|
||||||
.toolbar()
|
|
||||||
.read(cx)
|
|
||||||
.item_of_type::<BufferSearchBar>()
|
|
||||||
.expect("Buffer search bar should be deployed")
|
|
||||||
});
|
|
||||||
|
|
||||||
search_bar.read_with(cx.cx, |bar, cx| {
|
|
||||||
assert_eq!(bar.query_editor.read(cx).text(cx), "jumps");
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -6,7 +6,13 @@ use gpui::{actions, MutableAppContext, ViewContext};
|
||||||
use language::{AutoindentMode, SelectionGoal};
|
use language::{AutoindentMode, SelectionGoal};
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
use crate::{motion::Motion, state::Mode, utils::copy_selections_content, Vim};
|
use crate::{
|
||||||
|
motion::Motion,
|
||||||
|
object::Object,
|
||||||
|
state::{Mode, Operator},
|
||||||
|
utils::copy_selections_content,
|
||||||
|
Vim,
|
||||||
|
};
|
||||||
|
|
||||||
actions!(vim, [VisualDelete, VisualChange, VisualYank, VisualPaste]);
|
actions!(vim, [VisualDelete, VisualChange, VisualYank, VisualPaste]);
|
||||||
|
|
||||||
|
@ -17,13 +23,15 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||||
cx.add_action(paste);
|
cx.add_action(paste);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) {
|
pub fn visual_motion(motion: Motion, times: usize, cx: &mut MutableAppContext) {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||||
s.move_with(|map, selection| {
|
s.move_with(|map, selection| {
|
||||||
let (new_head, goal) = motion.move_point(map, selection.head(), selection.goal);
|
|
||||||
let was_reversed = selection.reversed;
|
let was_reversed = selection.reversed;
|
||||||
|
|
||||||
|
let (new_head, goal) =
|
||||||
|
motion.move_point(map, selection.head(), selection.goal, times);
|
||||||
selection.set_head(new_head, goal);
|
selection.set_head(new_head, goal);
|
||||||
|
|
||||||
if was_reversed && !selection.reversed {
|
if was_reversed && !selection.reversed {
|
||||||
|
@ -43,6 +51,36 @@ pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn visual_object(object: Object, cx: &mut MutableAppContext) {
|
||||||
|
Vim::update(cx, |vim, cx| {
|
||||||
|
if let Operator::Object { around } = vim.pop_operator(cx) {
|
||||||
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
|
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||||
|
s.move_with(|map, selection| {
|
||||||
|
let head = selection.head();
|
||||||
|
if let Some(mut range) = object.range(map, head, around) {
|
||||||
|
if !range.is_empty() {
|
||||||
|
if let Some((_, end)) = map.reverse_chars_at(range.end).next() {
|
||||||
|
range.end = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
if selection.is_empty() {
|
||||||
|
selection.start = range.start;
|
||||||
|
selection.end = range.end;
|
||||||
|
} else if selection.reversed {
|
||||||
|
selection.start = range.start;
|
||||||
|
} else {
|
||||||
|
selection.end = range.end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext<Workspace>) {
|
pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext<Workspace>) {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
|
@ -274,365 +312,151 @@ pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext<Workspace>
|
||||||
mod test {
|
mod test {
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
|
|
||||||
use crate::{state::Mode, vim_test_context::VimTestContext};
|
use crate::{
|
||||||
|
state::Mode,
|
||||||
|
test::{NeovimBackedTestContext, VimTestContext},
|
||||||
|
};
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
|
async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
|
||||||
let cx = VimTestContext::new(cx, true).await;
|
let mut cx = NeovimBackedTestContext::new(cx)
|
||||||
let mut cx = cx
|
.await
|
||||||
.binding(["v", "w", "j"])
|
.binding(["v", "w", "j"]);
|
||||||
.mode_after(Mode::Visual { line: false });
|
cx.assert_all(indoc! {"
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
The ˇquick brown
|
The ˇquick brown
|
||||||
fox jumps over
|
|
||||||
the lazy dog"},
|
|
||||||
indoc! {"
|
|
||||||
The «quick brown
|
|
||||||
fox jumps ˇ»over
|
|
||||||
the lazy dog"},
|
|
||||||
);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
The quick brown
|
|
||||||
fox jumps over
|
|
||||||
the ˇlazy dog"},
|
|
||||||
indoc! {"
|
|
||||||
The quick brown
|
|
||||||
fox jumps over
|
|
||||||
the «lazy ˇ»dog"},
|
|
||||||
);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
The quick brown
|
|
||||||
fox jumps ˇover
|
fox jumps ˇover
|
||||||
the lazy dog"},
|
the ˇlazy dog"})
|
||||||
indoc! {"
|
.await;
|
||||||
The quick brown
|
let mut cx = cx.binding(["v", "b", "k"]);
|
||||||
fox jumps «over
|
cx.assert_all(indoc! {"
|
||||||
ˇ»the lazy dog"},
|
|
||||||
);
|
|
||||||
let mut cx = cx
|
|
||||||
.binding(["v", "b", "k"])
|
|
||||||
.mode_after(Mode::Visual { line: false });
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
The ˇquick brown
|
The ˇquick brown
|
||||||
fox jumps over
|
|
||||||
the lazy dog"},
|
|
||||||
indoc! {"
|
|
||||||
«ˇThe q»uick brown
|
|
||||||
fox jumps over
|
|
||||||
the lazy dog"},
|
|
||||||
);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
The quick brown
|
|
||||||
fox jumps over
|
|
||||||
the ˇlazy dog"},
|
|
||||||
indoc! {"
|
|
||||||
The quick brown
|
|
||||||
«ˇfox jumps over
|
|
||||||
the l»azy dog"},
|
|
||||||
);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
The quick brown
|
|
||||||
fox jumps ˇover
|
fox jumps ˇover
|
||||||
the lazy dog"},
|
the ˇlazy dog"})
|
||||||
indoc! {"
|
.await;
|
||||||
The «ˇquick brown
|
|
||||||
fox jumps o»ver
|
|
||||||
the lazy dog"},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
|
async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
|
||||||
let cx = VimTestContext::new(cx, true).await;
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
let mut cx = cx.binding(["v", "w", "x"]);
|
|
||||||
cx.assert("The quick ˇbrown", "The quickˇ ");
|
|
||||||
let mut cx = cx.binding(["v", "w", "j", "x"]);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
The ˇquick brown
|
|
||||||
fox jumps over
|
|
||||||
the lazy dog"},
|
|
||||||
indoc! {"
|
|
||||||
The ˇver
|
|
||||||
the lazy dog"},
|
|
||||||
);
|
|
||||||
// Test pasting code copied on delete
|
|
||||||
cx.simulate_keystrokes(["j", "p"]);
|
|
||||||
cx.assert_editor_state(indoc! {"
|
|
||||||
The ver
|
|
||||||
the lˇquick brown
|
|
||||||
fox jumps oazy dog"});
|
|
||||||
|
|
||||||
cx.assert(
|
cx.assert_binding_matches(["v", "w", "x"], "The quick ˇbrown")
|
||||||
indoc! {"
|
.await;
|
||||||
The quick brown
|
cx.assert_binding_matches(
|
||||||
fox jumps over
|
["v", "w", "j", "x"],
|
||||||
the ˇlazy dog"},
|
|
||||||
indoc! {"
|
|
||||||
The quick brown
|
|
||||||
fox jumps over
|
|
||||||
the ˇog"},
|
|
||||||
);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
The quick brown
|
|
||||||
fox jumps ˇover
|
|
||||||
the lazy dog"},
|
|
||||||
indoc! {"
|
|
||||||
The quick brown
|
|
||||||
fox jumps ˇhe lazy dog"},
|
|
||||||
);
|
|
||||||
let mut cx = cx.binding(["v", "b", "k", "x"]);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
indoc! {"
|
||||||
The ˇquick brown
|
The ˇquick brown
|
||||||
fox jumps over
|
fox jumps over
|
||||||
the lazy dog"},
|
the lazy dog"},
|
||||||
indoc! {"
|
)
|
||||||
ˇuick brown
|
.await;
|
||||||
|
// Test pasting code copied on delete
|
||||||
|
cx.simulate_shared_keystrokes(["j", "p"]).await;
|
||||||
|
cx.assert_state_matches().await;
|
||||||
|
|
||||||
|
let mut cx = cx.binding(["v", "w", "j", "x"]);
|
||||||
|
cx.assert_all(indoc! {"
|
||||||
|
The ˇquick brown
|
||||||
fox jumps over
|
fox jumps over
|
||||||
the lazy dog"},
|
the ˇlazy dog"})
|
||||||
);
|
.await;
|
||||||
cx.assert(
|
let mut cx = cx.binding(["v", "b", "k", "x"]);
|
||||||
indoc! {"
|
cx.assert_all(indoc! {"
|
||||||
The quick brown
|
The ˇquick brown
|
||||||
fox jumps over
|
|
||||||
the ˇlazy dog"},
|
|
||||||
indoc! {"
|
|
||||||
The quick brown
|
|
||||||
ˇazy dog"},
|
|
||||||
);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
The quick brown
|
|
||||||
fox jumps ˇover
|
fox jumps ˇover
|
||||||
the lazy dog"},
|
the ˇlazy dog"})
|
||||||
indoc! {"
|
.await;
|
||||||
The ˇver
|
|
||||||
the lazy dog"},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
|
async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
|
||||||
let cx = VimTestContext::new(cx, true).await;
|
let mut cx = NeovimBackedTestContext::new(cx)
|
||||||
let mut cx = cx.binding(["shift-v", "x"]);
|
.await
|
||||||
cx.assert(
|
.binding(["shift-v", "x"]);
|
||||||
indoc! {"
|
cx.assert(indoc! {"
|
||||||
The quˇick brown
|
The quˇick brown
|
||||||
fox jumps over
|
fox jumps over
|
||||||
the lazy dog"},
|
the lazy dog"})
|
||||||
indoc! {"
|
.await;
|
||||||
fox juˇmps over
|
|
||||||
the lazy dog"},
|
|
||||||
);
|
|
||||||
// Test pasting code copied on delete
|
// Test pasting code copied on delete
|
||||||
cx.simulate_keystroke("p");
|
cx.simulate_shared_keystroke("p").await;
|
||||||
cx.assert_editor_state(indoc! {"
|
cx.assert_state_matches().await;
|
||||||
fox jumps over
|
|
||||||
ˇThe quick brown
|
|
||||||
the lazy dog"});
|
|
||||||
|
|
||||||
cx.assert(
|
cx.assert_all(indoc! {"
|
||||||
indoc! {"
|
|
||||||
The quick brown
|
The quick brown
|
||||||
fox juˇmps over
|
fox juˇmps over
|
||||||
the lazy dog"},
|
the laˇzy dog"})
|
||||||
indoc! {"
|
.await;
|
||||||
The quick brown
|
|
||||||
the laˇzy dog"},
|
|
||||||
);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
The quick brown
|
|
||||||
fox jumps over
|
|
||||||
the laˇzy dog"},
|
|
||||||
indoc! {"
|
|
||||||
The quick brown
|
|
||||||
fox juˇmps over"},
|
|
||||||
);
|
|
||||||
let mut cx = cx.binding(["shift-v", "j", "x"]);
|
let mut cx = cx.binding(["shift-v", "j", "x"]);
|
||||||
cx.assert(
|
cx.assert(indoc! {"
|
||||||
indoc! {"
|
|
||||||
The quˇick brown
|
The quˇick brown
|
||||||
fox jumps over
|
fox jumps over
|
||||||
the lazy dog"},
|
the lazy dog"})
|
||||||
"the laˇzy dog",
|
.await;
|
||||||
);
|
|
||||||
// Test pasting code copied on delete
|
// Test pasting code copied on delete
|
||||||
cx.simulate_keystroke("p");
|
cx.simulate_shared_keystroke("p").await;
|
||||||
cx.assert_editor_state(indoc! {"
|
cx.assert_state_matches().await;
|
||||||
the lazy dog
|
|
||||||
ˇThe quick brown
|
|
||||||
fox jumps over"});
|
|
||||||
|
|
||||||
cx.assert(
|
cx.assert_all(indoc! {"
|
||||||
indoc! {"
|
|
||||||
The quick brown
|
The quick brown
|
||||||
fox juˇmps over
|
fox juˇmps over
|
||||||
the lazy dog"},
|
the laˇzy dog"})
|
||||||
"The quˇick brown",
|
.await;
|
||||||
);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
The quick brown
|
|
||||||
fox jumps over
|
|
||||||
the laˇzy dog"},
|
|
||||||
indoc! {"
|
|
||||||
The quick brown
|
|
||||||
fox juˇmps over"},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_visual_change(cx: &mut gpui::TestAppContext) {
|
async fn test_visual_change(cx: &mut gpui::TestAppContext) {
|
||||||
let cx = VimTestContext::new(cx, true).await;
|
let mut cx = NeovimBackedTestContext::new(cx)
|
||||||
let mut cx = cx.binding(["v", "w", "c"]).mode_after(Mode::Insert);
|
.await
|
||||||
cx.assert("The quick ˇbrown", "The quick ˇ");
|
.binding(["v", "w", "c"]);
|
||||||
let mut cx = cx.binding(["v", "w", "j", "c"]).mode_after(Mode::Insert);
|
cx.assert("The quick ˇbrown").await;
|
||||||
cx.assert(
|
let mut cx = cx.binding(["v", "w", "j", "c"]);
|
||||||
indoc! {"
|
cx.assert_all(indoc! {"
|
||||||
The ˇquick brown
|
The ˇquick brown
|
||||||
fox jumps over
|
|
||||||
the lazy dog"},
|
|
||||||
indoc! {"
|
|
||||||
The ˇver
|
|
||||||
the lazy dog"},
|
|
||||||
);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
The quick brown
|
|
||||||
fox jumps over
|
|
||||||
the ˇlazy dog"},
|
|
||||||
indoc! {"
|
|
||||||
The quick brown
|
|
||||||
fox jumps over
|
|
||||||
the ˇog"},
|
|
||||||
);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
The quick brown
|
|
||||||
fox jumps ˇover
|
fox jumps ˇover
|
||||||
the lazy dog"},
|
the ˇlazy dog"})
|
||||||
indoc! {"
|
.await;
|
||||||
The quick brown
|
let mut cx = cx.binding(["v", "b", "k", "c"]);
|
||||||
fox jumps ˇhe lazy dog"},
|
cx.assert_all(indoc! {"
|
||||||
);
|
|
||||||
let mut cx = cx.binding(["v", "b", "k", "c"]).mode_after(Mode::Insert);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
The ˇquick brown
|
The ˇquick brown
|
||||||
fox jumps over
|
|
||||||
the lazy dog"},
|
|
||||||
indoc! {"
|
|
||||||
ˇuick brown
|
|
||||||
fox jumps over
|
|
||||||
the lazy dog"},
|
|
||||||
);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
The quick brown
|
|
||||||
fox jumps over
|
|
||||||
the ˇlazy dog"},
|
|
||||||
indoc! {"
|
|
||||||
The quick brown
|
|
||||||
ˇazy dog"},
|
|
||||||
);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
The quick brown
|
|
||||||
fox jumps ˇover
|
fox jumps ˇover
|
||||||
the lazy dog"},
|
the ˇlazy dog"})
|
||||||
indoc! {"
|
.await;
|
||||||
The ˇver
|
|
||||||
the lazy dog"},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
|
async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
|
||||||
let cx = VimTestContext::new(cx, true).await;
|
let mut cx = NeovimBackedTestContext::new(cx)
|
||||||
let mut cx = cx.binding(["shift-v", "c"]).mode_after(Mode::Insert);
|
.await
|
||||||
cx.assert(
|
.binding(["shift-v", "c"]);
|
||||||
indoc! {"
|
cx.assert(indoc! {"
|
||||||
The quˇick brown
|
The quˇick brown
|
||||||
fox jumps over
|
fox jumps over
|
||||||
the lazy dog"},
|
the lazy dog"})
|
||||||
indoc! {"
|
.await;
|
||||||
ˇ
|
|
||||||
fox jumps over
|
|
||||||
the lazy dog"},
|
|
||||||
);
|
|
||||||
// Test pasting code copied on change
|
// Test pasting code copied on change
|
||||||
cx.simulate_keystrokes(["escape", "j", "p"]);
|
cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
|
||||||
cx.assert_editor_state(indoc! {"
|
cx.assert_state_matches().await;
|
||||||
|
|
||||||
fox jumps over
|
|
||||||
ˇThe quick brown
|
|
||||||
the lazy dog"});
|
|
||||||
|
|
||||||
cx.assert(
|
cx.assert_all(indoc! {"
|
||||||
indoc! {"
|
|
||||||
The quick brown
|
The quick brown
|
||||||
fox juˇmps over
|
fox juˇmps over
|
||||||
the lazy dog"},
|
the laˇzy dog"})
|
||||||
indoc! {"
|
.await;
|
||||||
The quick brown
|
let mut cx = cx.binding(["shift-v", "j", "c"]);
|
||||||
ˇ
|
cx.assert(indoc! {"
|
||||||
the lazy dog"},
|
|
||||||
);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
The quick brown
|
|
||||||
fox jumps over
|
|
||||||
the laˇzy dog"},
|
|
||||||
indoc! {"
|
|
||||||
The quick brown
|
|
||||||
fox jumps over
|
|
||||||
ˇ"},
|
|
||||||
);
|
|
||||||
let mut cx = cx.binding(["shift-v", "j", "c"]).mode_after(Mode::Insert);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
The quˇick brown
|
The quˇick brown
|
||||||
fox jumps over
|
fox jumps over
|
||||||
the lazy dog"},
|
the lazy dog"})
|
||||||
indoc! {"
|
.await;
|
||||||
ˇ
|
|
||||||
the lazy dog"},
|
|
||||||
);
|
|
||||||
// Test pasting code copied on delete
|
// Test pasting code copied on delete
|
||||||
cx.simulate_keystrokes(["escape", "j", "p"]);
|
cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
|
||||||
cx.assert_editor_state(indoc! {"
|
cx.assert_state_matches().await;
|
||||||
|
|
||||||
the lazy dog
|
cx.assert_all(indoc! {"
|
||||||
ˇThe quick brown
|
|
||||||
fox jumps over"});
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
The quick brown
|
The quick brown
|
||||||
fox juˇmps over
|
fox juˇmps over
|
||||||
the lazy dog"},
|
the laˇzy dog"})
|
||||||
indoc! {"
|
.await;
|
||||||
The quick brown
|
|
||||||
ˇ"},
|
|
||||||
);
|
|
||||||
cx.assert(
|
|
||||||
indoc! {"
|
|
||||||
The quick brown
|
|
||||||
fox jumps over
|
|
||||||
the laˇzy dog"},
|
|
||||||
indoc! {"
|
|
||||||
The quick brown
|
|
||||||
fox jumps over
|
|
||||||
ˇ"},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
@ -741,7 +565,7 @@ mod test {
|
||||||
cx.assert_state(
|
cx.assert_state(
|
||||||
indoc! {"
|
indoc! {"
|
||||||
The quick brown
|
The quick brown
|
||||||
fox jumpsˇjumps over
|
fox jumpsjumpˇs over
|
||||||
the lazy dog"},
|
the lazy dog"},
|
||||||
Mode::Normal,
|
Mode::Normal,
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"This is a test"},{"Mode":"Normal"},{"Selection":{"start":[0,13],"end":[0,13]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_a.json
Normal file
1
crates/vim/test_data/test_a.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":"The quick"},{"Mode":"Insert"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Insert"},{"Text":"The quick"},{"Mode":"Insert"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Insert"}]
|
1
crates/vim/test_data/test_b.json
Normal file
1
crates/vim/test_data/test_b.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,10],"end":[3,10]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,10],"end":[3,10]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_backspace.json
Normal file
1
crates/vim/test_data/test_backspace.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_cc.json
Normal file
1
crates/vim/test_data/test_cc.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nbrown fox\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\n\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick\n\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
|
1
crates/vim/test_data/test_change_sentence_object.json
Normal file
1
crates/vim/test_data/test_change_sentence_object.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
crates/vim/test_data/test_change_word_object.json
Normal file
1
crates/vim/test_data/test_change_word_object.json
Normal file
File diff suppressed because one or more lines are too long
1
crates/vim/test_data/test_dd.json
Normal file
1
crates/vim/test_data/test_dd.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_delete_left.json
Normal file
1
crates/vim/test_data/test_delete_left.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":"Test"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"est"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Tst"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":"Tet"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"},{"Text":"Test\ntest"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_delete_sentence_object.json
Normal file
1
crates/vim/test_data/test_delete_sentence_object.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
crates/vim/test_data/test_delete_to_end_of_line.json
Normal file
1
crates/vim/test_data/test_delete_to_end_of_line.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":"The q\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_delete_word_object.json
Normal file
1
crates/vim/test_data/test_delete_word_object.json
Normal file
File diff suppressed because one or more lines are too long
1
crates/vim/test_data/test_e.json
Normal file
1
crates/vim/test_data/test_e.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,14],"end":[0,14]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,8],"end":[3,8]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,13],"end":[3,13]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[4,2],"end":[4,2]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[4,2],"end":[4,2]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,14],"end":[0,14]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,14],"end":[0,14]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,14],"end":[0,14]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,8],"end":[3,8]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,13],"end":[3,13]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[4,2],"end":[4,2]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[4,2],"end":[4,2]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_enter_visual_mode.json
Normal file
1
crates/vim/test_data/test_enter_visual_mode.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":"The quick brown\nfox jumps over\nthe lazy dog"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,4],"end":[1,10]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown\nfox jumps over\nthe lazy dog"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[1,10],"end":[2,0]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown\nfox jumps over\nthe lazy dog"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[2,4],"end":[2,9]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown\nfox jumps over\nthe lazy dog"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[0,4],"end":[0,0]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown\nfox jumps over\nthe lazy dog"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[1,10],"end":[0,4]}},{"Mode":{"Visual":{"line":false}}},{"Text":"The quick brown\nfox jumps over\nthe lazy dog"},{"Mode":{"Visual":{"line":false}}},{"Selection":{"start":[2,4],"end":[1,0]}},{"Mode":{"Visual":{"line":false}}}]
|
1
crates/vim/test_data/test_gg.json
Normal file
1
crates/vim/test_data/test_gg.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"},{"Text":"\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"\n\nbrown fox jumps\nover the lazydog"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_h.json
Normal file
1
crates/vim/test_data/test_h.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_insert_end_of_line.json
Normal file
1
crates/vim/test_data/test_insert_end_of_line.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":"\nThe quick\nbrown fox "},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nThe quick\nbrown fox "},{"Mode":"Insert"},{"Selection":{"start":[1,9],"end":[1,9]}},{"Mode":"Insert"},{"Text":"\nThe quick\nbrown fox "},{"Mode":"Insert"},{"Selection":{"start":[2,10],"end":[2,10]}},{"Mode":"Insert"}]
|
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":"The quick"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":" The quick"},{"Mode":"Insert"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Insert"},{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nThe quick"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"}]
|
1
crates/vim/test_data/test_insert_line_above.json
Normal file
1
crates/vim/test_data/test_insert_line_above.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":"\n"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nThe quick"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nThe quick\nbrown fox\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\n\nbrown fox\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\n\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick\n\n\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
|
1
crates/vim/test_data/test_j.json
Normal file
1
crates/vim/test_data/test_j.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,5],"end":[1,5]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,8],"end":[1,8]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_jump_to_end.json
Normal file
1
crates/vim/test_data/test_jump_to_end.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[3,4],"end":[3,4]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[3,4],"end":[3,4]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown fox jumps\nover the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[3,16],"end":[3,16]}},{"Mode":"Normal"},{"Text":"The quick\n\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Normal"},{"Text":"The quick\n\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"}]
|
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":" The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"\nThe quick"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":" \nThe quick"},{"Mode":"Normal"},{"Selection":{"start":[0,3],"end":[0,3]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_jump_to_line_boundaries.json
Normal file
1
crates/vim/test_data/test_jump_to_line_boundaries.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[1,4],"end":[1,4]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[1,4],"end":[1,4]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_k.json
Normal file
1
crates/vim/test_data/test_k.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":"The quick\nbrown fox jumps"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox jumps"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox jumps"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox jumps"},{"Mode":"Normal"},{"Selection":{"start":[0,7],"end":[0,7]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox jumps"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_l.json
Normal file
1
crates/vim/test_data/test_l.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,1],"end":[0,1]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[0,8],"end":[0,8]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[1,1],"end":[1,1]}},{"Mode":"Normal"},{"Text":"The quick\nbrown"},{"Mode":"Normal"},{"Selection":{"start":[1,4],"end":[1,4]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_neovim.json
Normal file
1
crates/vim/test_data/test_neovim.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"test"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_o.json
Normal file
1
crates/vim/test_data/test_o.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":"\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\n\nbrown fox\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\n\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"},{"Text":"The quick\n\n\nbrown fox"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"fn test() {\n println!();\n \n}\n"},{"Mode":"Insert"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Insert"},{"Text":"fn test() {\n\n println!();\n}"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
|
1
crates/vim/test_data/test_p.json
Normal file
1
crates/vim/test_data/test_p.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":"The quick brown\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick brown\nthe lazy dog\nfox jumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps overjumps o\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[1,20],"end":[1,20]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_repeated_cb.json
Normal file
1
crates/vim/test_data/test_repeated_cb.json
Normal file
File diff suppressed because one or more lines are too long
1
crates/vim/test_data/test_repeated_ce.json
Normal file
1
crates/vim/test_data/test_repeated_ce.json
Normal file
File diff suppressed because one or more lines are too long
1
crates/vim/test_data/test_repeated_cj.json
Normal file
1
crates/vim/test_data/test_repeated_cj.json
Normal file
File diff suppressed because one or more lines are too long
1
crates/vim/test_data/test_repeated_cl.json
Normal file
1
crates/vim/test_data/test_repeated_cl.json
Normal file
File diff suppressed because one or more lines are too long
1
crates/vim/test_data/test_repeated_word.json
Normal file
1
crates/vim/test_data/test_repeated_word.json
Normal file
File diff suppressed because one or more lines are too long
1
crates/vim/test_data/test_visual_change.json
Normal file
1
crates/vim/test_data/test_visual_change.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":"The quick "},{"Mode":"Insert"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Insert"},{"Text":"The ver\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Insert"},{"Text":"The quick brown\nfox jumps he lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[1,10],"end":[1,10]}},{"Mode":"Insert"},{"Text":"The quick brown\nfox jumps over\nthe og"},{"Mode":"Insert"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Insert"},{"Text":"uick brown\nfox jumps over\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The ver\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Insert"},{"Text":"The quick brown\nazy dog"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
|
1
crates/vim/test_data/test_visual_delete.json
Normal file
1
crates/vim/test_data/test_visual_delete.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":"The quick "},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"The ver\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The ver\nthe lquick brown\nfox jumps oazy dog"},{"Mode":"Normal"},{"Selection":{"start":[1,5],"end":[1,5]}},{"Mode":"Normal"},{"Text":"The ver\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps over\nthe og"},{"Mode":"Normal"},{"Selection":{"start":[2,4],"end":[2,4]}},{"Mode":"Normal"},{"Text":"uick brown\nfox jumps over\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"The ver\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"The quick brown\nazy dog"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_visual_line_change.json
Normal file
1
crates/vim/test_data/test_visual_line_change.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":"\nfox jumps over\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nfox jumps over\nThe quick brown\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick brown\n\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick brown\nfox jumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"},{"Text":"\nthe lazy dog"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nthe lazy dog\nThe quick brown\nfox jumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick brown\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick brown\nfox jumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Insert"}]
|
1
crates/vim/test_data/test_visual_line_delete.json
Normal file
1
crates/vim/test_data/test_visual_line_delete.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":"fox jumps over\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"fox jumps over\nThe quick brown\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick brown\nthe lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps over"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"the lazy dog"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"the lazy dog\nThe quick brown\nfox jumps over"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick brown"},{"Mode":"Normal"},{"Selection":{"start":[0,6],"end":[0,6]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps over"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_visual_sentence_object.json
Normal file
1
crates/vim/test_data/test_visual_sentence_object.json
Normal file
File diff suppressed because one or more lines are too long
1
crates/vim/test_data/test_visual_word_object.json
Normal file
1
crates/vim/test_data/test_visual_word_object.json
Normal file
File diff suppressed because one or more lines are too long
1
crates/vim/test_data/test_w.json
Normal file
1
crates/vim/test_data/test_w.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,9],"end":[0,9]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,10],"end":[3,10]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[4,0],"end":[4,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[4,2],"end":[4,2]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[4,2],"end":[4,2]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[3,10],"end":[3,10]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[4,0],"end":[4,0]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[4,2],"end":[4,2]}},{"Mode":"Normal"},{"Text":"The quick-brown\n\n\nfox_jumps over\nthe"},{"Mode":"Normal"},{"Selection":{"start":[4,2],"end":[4,2]}},{"Mode":"Normal"}]
|
1
crates/vim/test_data/test_x.json
Normal file
1
crates/vim/test_data/test_x.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[{"Text":"est"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"Tet"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"},{"Text":"Tes"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"},{"Text":"Tes\ntest"},{"Mode":"Normal"},{"Selection":{"start":[0,2],"end":[0,2]}},{"Mode":"Normal"}]
|
30
script/amplitude_release/main.py
Normal file
30
script/amplitude_release/main.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import datetime
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from amplitude_python_sdk.v2.clients.releases_client import ReleasesAPIClient
|
||||||
|
from amplitude_python_sdk.v2.models.releases import Release
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
version = sys.argv[1]
|
||||||
|
version = version.removeprefix("v")
|
||||||
|
|
||||||
|
api_key = sys.argv[2]
|
||||||
|
secret_key = sys.argv[3]
|
||||||
|
|
||||||
|
current_datetime = datetime.datetime.now(datetime.timezone.utc)
|
||||||
|
current_datetime = current_datetime.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
release = Release(
|
||||||
|
title=version,
|
||||||
|
version=version,
|
||||||
|
release_start=current_datetime,
|
||||||
|
created_by="GitHub Release Workflow",
|
||||||
|
chart_visibility=True
|
||||||
|
)
|
||||||
|
|
||||||
|
ReleasesAPIClient(api_key=api_key, secret_key=secret_key).create(release)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
1
script/amplitude_release/requirements.txt
Normal file
1
script/amplitude_release/requirements.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
amplitude-python-sdk==0.2.0
|
|
@ -1,4 +1,5 @@
|
||||||
import Theme from "../themes/common/theme";
|
import Theme from "../themes/common/theme";
|
||||||
|
import { withOpacity } from "../utils/color";
|
||||||
import {
|
import {
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
border,
|
border,
|
||||||
|
@ -170,6 +171,24 @@ export default function editor(theme: Theme) {
|
||||||
background: backgroundColor(theme, "on500"),
|
background: backgroundColor(theme, "on500"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
scrollbar: {
|
||||||
|
width: 12,
|
||||||
|
minHeightFactor: 1.0,
|
||||||
|
track: {
|
||||||
|
border: {
|
||||||
|
left: true,
|
||||||
|
width: 1,
|
||||||
|
color: borderColor(theme, "secondary"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
thumb: {
|
||||||
|
background: withOpacity(borderColor(theme, "secondary"), 0.5),
|
||||||
|
border: {
|
||||||
|
width: 1,
|
||||||
|
color: withOpacity(borderColor(theme, 'muted'), 0.5),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
compositionMark: {
|
compositionMark: {
|
||||||
underline: {
|
underline: {
|
||||||
thickness: 1.0,
|
thickness: 1.0,
|
||||||
|
|
|
@ -123,7 +123,7 @@ export function createTheme(
|
||||||
const borderColor = {
|
const borderColor = {
|
||||||
primary: sample(ramps.neutral, isLight ? 1.5 : 0),
|
primary: sample(ramps.neutral, isLight ? 1.5 : 0),
|
||||||
secondary: sample(ramps.neutral, isLight ? 1.25 : 1),
|
secondary: sample(ramps.neutral, isLight ? 1.25 : 1),
|
||||||
muted: sample(ramps.neutral, isLight ? 1 : 3),
|
muted: sample(ramps.neutral, isLight ? 1.25 : 3),
|
||||||
active: sample(ramps.neutral, isLight ? 4 : 3),
|
active: sample(ramps.neutral, isLight ? 4 : 3),
|
||||||
onMedia: withOpacity(darkest, 0.1),
|
onMedia: withOpacity(darkest, 0.1),
|
||||||
ok: sample(ramps.green, 0.3),
|
ok: sample(ramps.green, 0.3),
|
||||||
|
|
Loading…
Reference in a new issue