diff --git a/CHANGELOG.md b/CHANGELOG.md index 499f612..a94b530 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Parsing issues with URLs starting in the first or ending in the last column - URLs stopping at double-width characters - Fix `start_maximized` option on X11 +- Error when parsing URLs ending with Unicode outside of the ascii range ## Version 0.2.9 diff --git a/src/event.rs b/src/event.rs index 121c0c4..f7d9f22 100644 --- a/src/event.rs +++ b/src/event.rs @@ -432,6 +432,7 @@ impl Processor { processor.ctx.terminal.dirty = true; processor.ctx.terminal.next_is_urgent = Some(false); } else { + processor.ctx.terminal.reset_url_highlight(); processor.ctx.terminal.dirty = true; *hide_mouse = false; } diff --git a/src/grid/mod.rs b/src/grid/mod.rs index 272ea34..a190353 100644 --- a/src/grid/mod.rs +++ b/src/grid/mod.rs @@ -15,9 +15,9 @@ //! A specialized 2d grid implementation optimized for use in a terminal. use std::cmp::{min, max, Ordering}; -use std::ops::{Deref, Range, Index, IndexMut, RangeTo, RangeFrom, RangeFull}; +use std::ops::{Deref, Range, Index, IndexMut, RangeTo, RangeFrom, RangeFull, RangeInclusive}; -use crate::index::{self, Point, Line, Column, IndexRange, PointIterator}; +use crate::index::{self, Point, Line, Column, IndexRange}; use crate::selection::Selection; mod row; @@ -60,7 +60,8 @@ impl ::std::cmp::PartialEq for Grid { self.lines.eq(&other.lines) && self.display_offset.eq(&other.display_offset) && self.scroll_limit.eq(&other.scroll_limit) && - self.selection.eq(&other.selection) + self.selection.eq(&other.selection) && + self.url_highlight.eq(&other.url_highlight) } } @@ -103,6 +104,10 @@ pub struct Grid { #[serde(default)] max_scroll_limit: usize, + + /// Range for URL hover highlights + #[serde(default)] + pub url_highlight: Option>, } #[derive(Copy, Clone)] @@ -132,6 +137,7 @@ impl Grid { scroll_limit: 0, selection: None, max_scroll_limit: scrollback, + url_highlight: None, } } @@ -387,6 +393,7 @@ impl Grid { let prev = self.lines; self.selection = None; + self.url_highlight = None; self.raw.rotate(*prev as isize - *target as isize); self.raw.shrink_visible_lines(target); self.lines = target; @@ -422,6 +429,7 @@ impl Grid { if let Some(ref mut selection) = self.selection { selection.rotate(-(*positions as isize)); } + self.url_highlight = None; self.decrease_scroll_limit(*positions); @@ -473,6 +481,7 @@ impl Grid { if let Some(ref mut selection) = self.selection { selection.rotate(*positions as isize); } + self.url_highlight = None; // // This next loop swaps "fixed" lines outside of a scroll region // // back into place after the rotation. The work is done in buffer- @@ -517,6 +526,7 @@ impl Grid { self.display_offset = 0; self.selection = None; + self.url_highlight = None; } } @@ -573,7 +583,7 @@ impl Grid { pub fn iter_from(&self, point: Point) -> GridIterator<'_, T> { GridIterator { grid: self, - point_iter: point.iter(self.num_cols() - 1, self.len() - 1), + cur: point, } } @@ -589,27 +599,50 @@ impl Grid { } pub struct GridIterator<'a, T> { - point_iter: PointIterator, + /// Immutable grid reference grid: &'a Grid, -} -impl<'a, T> GridIterator<'a, T> { - pub fn cur(&self) -> Point { - self.point_iter.cur - } + /// Current position of the iterator within the grid. + pub cur: Point, } impl<'a, T> Iterator for GridIterator<'a, T> { type Item = &'a T; fn next(&mut self) -> Option { - self.point_iter.next().map(|p| &self.grid[p.line][p.col]) + let last_col = self.grid.num_cols() - Column(1); + match self.cur { + Point { line, col } if line == 0 && col == last_col => None, + Point { col, .. } if + (col == last_col) => { + self.cur.line -= 1; + self.cur.col = Column(0); + Some(&self.grid[self.cur.line][self.cur.col]) + }, + _ => { + self.cur.col += Column(1); + Some(&self.grid[self.cur.line][self.cur.col]) + } + } } } impl<'a, T> BidirectionalIterator for GridIterator<'a, T> { fn prev(&mut self) -> Option { - self.point_iter.prev().map(|p| &self.grid[p.line][p.col]) + let num_cols = self.grid.num_cols(); + + match self.cur { + Point { line, col: Column(0) } if line == self.grid.len() - 1 => None, + Point { col: Column(0), .. } => { + self.cur.line += 1; + self.cur.col = num_cols - Column(1); + Some(&self.grid[self.cur.line][self.cur.col]) + }, + _ => { + self.cur.col -= Column(1); + Some(&self.grid[self.cur.line][self.cur.col]) + } + } } } diff --git a/src/grid/tests.rs b/src/grid/tests.rs index 33d772a..82edda6 100644 --- a/src/grid/tests.rs +++ b/src/grid/tests.rs @@ -112,8 +112,8 @@ fn test_iter() { assert_eq!(None, iter.prev()); assert_eq!(Some(&1), iter.next()); - assert_eq!(Column(1), iter.cur().col); - assert_eq!(4, iter.cur().line); + assert_eq!(Column(1), iter.cur.col); + assert_eq!(4, iter.cur.line); assert_eq!(Some(&2), iter.next()); assert_eq!(Some(&3), iter.next()); @@ -121,12 +121,12 @@ fn test_iter() { // test linewrapping assert_eq!(Some(&5), iter.next()); - assert_eq!(Column(0), iter.cur().col); - assert_eq!(3, iter.cur().line); + assert_eq!(Column(0), iter.cur.col); + assert_eq!(3, iter.cur.line); assert_eq!(Some(&4), iter.prev()); - assert_eq!(Column(4), iter.cur().col); - assert_eq!(4, iter.cur().line); + assert_eq!(Column(4), iter.cur.col); + assert_eq!(4, iter.cur.line); // test that iter ends at end of grid diff --git a/src/index.rs b/src/index.rs index 6dbfbc4..0004454 100644 --- a/src/index.rs +++ b/src/index.rs @@ -19,8 +19,6 @@ use std::cmp::{Ord, Ordering}; use std::fmt; use std::ops::{self, Deref, Range, RangeInclusive, Add, Sub, AddAssign, SubAssign}; -use crate::grid::BidirectionalIterator; - /// The side of a cell #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum Side { @@ -72,67 +70,6 @@ impl From for Point { } } -impl Point -where - T: Copy + Default + SubAssign + PartialEq, -{ - pub fn iter(&self, last_col: Column, last_line: T) -> PointIterator { - PointIterator { - cur: *self, - last_col, - last_line, - } - } -} - -pub struct PointIterator { - pub cur: Point, - last_col: Column, - last_line: T, -} - -impl Iterator for PointIterator -where - T: Copy + Default + SubAssign + PartialEq, -{ - type Item = Point; - - fn next(&mut self) -> Option { - match self.cur { - Point { line, col } if line == Default::default() && col == self.last_col => None, - Point { col, .. } if col == self.last_col => { - self.cur.line -= 1; - self.cur.col = Column(0); - Some(self.cur) - }, - _ => { - self.cur.col += Column(1); - Some(self.cur) - } - } - } -} - -impl BidirectionalIterator for PointIterator -where - T: Copy + Default + AddAssign + SubAssign + PartialEq, -{ - fn prev(&mut self) -> Option { - match self.cur { - Point { line, col: Column(0) } if line == self.last_line => None, - Point { col: Column(0), .. } => { - self.cur.line += 1; - self.cur.col = self.last_col; - Some(self.cur) - }, - _ => { - self.cur.col -= Column(1); - Some(self.cur) - } - } - } -} - /// A line /// /// Newtype to avoid passing values incorrectly @@ -163,6 +100,16 @@ impl fmt::Display for Column { #[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Ord, PartialOrd, Serialize, Deserialize)] pub struct Linear(pub usize); +impl Linear { + pub fn new(columns: Column, column: Column, line: Line) -> Self { + Linear(line.0 * columns.0 + column.0) + } + + pub fn from_point(columns: Column, point: Point) -> Self { + Linear(point.line * columns.0 + point.col.0) + } +} + impl fmt::Display for Linear { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Linear({})", self.0) diff --git a/src/input.rs b/src/input.rs index 9fe8e6b..51e4e71 100644 --- a/src/input.rs +++ b/src/input.rs @@ -21,9 +21,10 @@ use std::borrow::Cow; use std::mem; use std::time::Instant; -use std::iter::once; +use std::ops::RangeInclusive; use copypasta::{Clipboard, Load, Buffer as ClipboardBuffer}; +use unicode_width::UnicodeWidthStr; use glutin::{ ElementState, KeyboardInput, ModifiersState, MouseButton, MouseCursor, MouseScrollDelta, TouchPhase, @@ -32,10 +33,9 @@ use glutin::{ use crate::config::{self, Key}; use crate::grid::Scroll; use crate::event::{ClickState, Mouse}; -use crate::index::{Line, Column, Side, Point}; -use crate::term::{Term, SizeInfo, Search, UrlHoverSaveState}; +use crate::index::{Line, Column, Side, Point, Linear}; +use crate::term::{Term, SizeInfo, Search}; use crate::term::mode::TermMode; -use crate::term::cell::Flags; use crate::util::fmt::Red; use crate::util::start_daemon; use crate::message_bar::{self, Message}; @@ -448,45 +448,30 @@ impl<'a, A: ActionContext + 'a> Processor<'a, A> { None }; - if let Some(Url { origin, len, .. }) = url { - let mouse_cursor = if self.ctx.terminal().mode().intersects(mouse_mode) { - MouseCursor::Default - } else { - MouseCursor::Text - }; - + if let Some(Url { origin, text }) = url { let cols = self.ctx.size_info().cols().0; - let last_line = self.ctx.size_info().lines().0 - 1; // Calculate the URL's start position - let col = (cols + point.col.0 - origin % cols) % cols; - let line = last_line - point.line.0 + (origin + cols - point.col.0 - 1) / cols; - let start = Point::new(line, Column(col)); + let lines_before = (origin + cols - point.col.0 - 1) / cols; + let (start_col, start_line) = if lines_before > point.line.0 { + (0, 0) + } else { + let start_col = (cols + point.col.0 - origin % cols) % cols; + let start_line = point.line.0 - lines_before; + (start_col, start_line) + }; + let start = Point::new(start_line, Column(start_col)); - // Update URLs only on change, so they don't all get marked as underlined - if self.ctx.terminal().url_highlight_start() == Some(start) { - return; - } + // Calculate the URL's end position + let len = text.width(); + let end_col = (point.col.0 + len - origin) % cols - 1; + let end_line = point.line.0 + (point.col.0 + len - origin) / cols; + let end = Point::new(end_line, Column(end_col)); - // Since the URL changed without reset, we need to clear the previous underline - self.ctx.terminal_mut().reset_url_highlight(); - - // Underline all cells and store their current underline state - let mut underlined = Vec::with_capacity(len); - let iter = once(start).chain(start.iter(Column(cols - 1), last_line)); - for point in iter.take(len) { - let cell = &mut self.ctx.terminal_mut().grid_mut()[point.line][point.col]; - underlined.push(cell.flags.contains(Flags::UNDERLINE)); - cell.flags.insert(Flags::UNDERLINE); - } - - // Save the higlight state for restoring it again - self.ctx.terminal_mut().set_url_highlight(UrlHoverSaveState { - mouse_cursor, - underlined, - start, - }); + let start = Linear::from_point(Column(cols), start); + let end = Linear::from_point(Column(cols), end); + self.ctx.terminal_mut().set_url_highlight(RangeInclusive::new(start, end)); self.ctx.terminal_mut().set_mouse_cursor(MouseCursor::Hand); self.ctx.terminal_mut().dirty = true; } else { diff --git a/src/term/mod.rs b/src/term/mod.rs index fc2b172..8910504 100644 --- a/src/term/mod.rs +++ b/src/term/mod.rs @@ -17,7 +17,6 @@ use std::ops::{Range, Index, IndexMut, RangeInclusive}; use std::{ptr, io, mem}; use std::cmp::{min, max}; use std::time::{Duration, Instant}; -use std::iter::once; use arraydeque::ArrayDeque; use unicode_width::UnicodeWidthChar; @@ -27,7 +26,7 @@ use font::{self, Size}; use crate::ansi::{self, Color, NamedColor, Attr, Handler, CharsetIndex, StandardCharset, CursorStyle}; use crate::grid::{ BidirectionalIterator, DisplayIter, Grid, GridCell, IndexRegion, Indexed, Scroll, - ViewportPosition, + ViewportPosition }; use crate::index::{self, Point, Column, Line, IndexRange, Contains, Linear}; use crate::selection::{self, Selection, Locations}; @@ -71,11 +70,11 @@ impl Search for Term { break; } - if iter.cur().col == last_col && !cell.flags.contains(cell::Flags::WRAPLINE) { + if iter.cur.col == last_col && !cell.flags.contains(cell::Flags::WRAPLINE) { break; // cut off if on new line or hit escape char } - point = iter.cur(); + point = iter.cur; } point @@ -93,7 +92,7 @@ impl Search for Term { break; } - point = iter.cur(); + point = iter.cur; if point.col == last_col && !cell.flags.contains(cell::Flags::WRAPLINE) { break; // cut off if on new line or hit escape char @@ -120,7 +119,7 @@ impl Search for Term { // Find URLs let mut url_parser = UrlParser::new(); while let Some(cell) = iterb.prev() { - if (iterb.cur().col == last_col && !cell.flags.contains(cell::Flags::WRAPLINE)) + if (iterb.cur.col == last_col && !cell.flags.contains(cell::Flags::WRAPLINE)) || url_parser.advance_left(cell) { break; @@ -129,7 +128,7 @@ impl Search for Term { while let Some(cell) = iterf.next() { if url_parser.advance_right(cell) - || (iterf.cur().col == last_col && !cell.flags.contains(cell::Flags::WRAPLINE)) + || (iterf.cur.col == last_col && !cell.flags.contains(cell::Flags::WRAPLINE)) { break; } @@ -164,6 +163,7 @@ pub struct RenderableCellsIter<'a> { config: &'a Config, colors: &'a color::List, selection: Option>, + url_highlight: &'a Option>, cursor_cells: ArrayDeque<[Indexed; 3]>, } @@ -173,15 +173,14 @@ impl<'a> RenderableCellsIter<'a> { /// The cursor and terminal mode are required for properly displaying the /// cursor. fn new<'b>( - grid: &'b Grid, - cursor: &'b Point, - colors: &'b color::List, - mode: TermMode, + term: &'b Term, config: &'b Config, selection: Option, cursor_style: CursorStyle, ) -> RenderableCellsIter<'b> { - let cursor_offset = grid.line_to_offset(cursor.line); + let grid = &term.grid; + + let cursor_offset = grid.line_to_offset(term.cursor.point.line); let inner = grid.display_iter(); let mut selection_range = None; @@ -224,8 +223,8 @@ impl<'a> RenderableCellsIter<'a> { } let cols = grid.num_cols(); - let start = Linear(start.line.0 * cols.0 + start.col.0); - let end = Linear(end.line.0 * cols.0 + end.col.0); + let start = Linear::from_point(cols, start.into()); + let end = Linear::from_point(cols, end.into()); // Update the selection selection_range = Some(RangeInclusive::new(start, end)); @@ -233,14 +232,15 @@ impl<'a> RenderableCellsIter<'a> { } RenderableCellsIter { - cursor, + cursor: &term.cursor.point, cursor_offset, grid, inner, - mode, + mode: term.mode, selection: selection_range, + url_highlight: &grid.url_highlight, config, - colors, + colors: &term.colors, cursor_cells: ArrayDeque::new(), }.initialize(cursor_style) } @@ -435,7 +435,7 @@ impl<'a> Iterator for RenderableCellsIter<'a> { fn next(&mut self) -> Option { loop { // Handle cursor - let (cell, selected) = if self.cursor_offset == self.inner.offset() && + let (cell, selected, highlighted) = if self.cursor_offset == self.inner.offset() && self.inner.column() == self.cursor.col { // Cursor cell @@ -448,11 +448,11 @@ impl<'a> Iterator for RenderableCellsIter<'a> { if self.cursor_cells.is_empty() { self.inner.next(); } - (cell, false) + (cell, false, false) } else { let cell = self.inner.next()?; - let index = Linear(cell.line.0 * self.grid.num_cols().0 + cell.column.0); + let index = Linear::new(self.grid.num_cols(), cell.column, cell.line); let selected = self.selection.as_ref() .map(|range| range.contains_(index)) @@ -463,7 +463,12 @@ impl<'a> Iterator for RenderableCellsIter<'a> { continue; } - (cell, selected) + // Underline URL highlights + let highlighted = self.url_highlight.as_ref() + .map(|range| range.contains_(index)) + .unwrap_or(false); + + (cell, selected, highlighted) }; // Lookup RGB values @@ -488,14 +493,19 @@ impl<'a> Iterator for RenderableCellsIter<'a> { fg_rgb = col; } + let mut flags = cell.flags; + if highlighted { + flags.insert(Flags::UNDERLINE); + } + return Some(RenderableCell { line: cell.line, column: cell.column, - flags: cell.flags, chars: cell.chars(), fg: fg_rgb, bg: bg_rgb, bg_alpha, + flags, }) } } @@ -824,16 +834,6 @@ pub struct Term { /// Hint that Alacritty should be closed should_exit: bool, - - /// URL highlight save state - url_hover_save: Option, -} - -/// Temporary save state for restoring mouse cursor and underline after unhovering a URL. -pub struct UrlHoverSaveState { - pub mouse_cursor: MouseCursor, - pub underlined: Vec, - pub start: Point, } /// Terminal size info @@ -910,8 +910,10 @@ impl Term { self.next_title.take() } + #[inline] pub fn scroll_display(&mut self, scroll: Scroll) { self.grid.scroll_display(scroll); + self.reset_url_highlight(); self.dirty = true; } @@ -966,7 +968,6 @@ impl Term { auto_scroll: config.scrolling().auto_scroll, message_buffer, should_exit: false, - url_hover_save: None, } } @@ -1162,7 +1163,8 @@ impl Term { &self.grid } - /// Mutable access to the raw grid data structure + /// Mutable access for swapping out the grid during tests + #[cfg(test)] pub fn grid_mut(&mut self) -> &mut Grid { &mut self.grid } @@ -1191,10 +1193,7 @@ impl Term { }; RenderableCellsIter::new( - &self.grid, - &self.cursor.point, - &self.colors, - self.mode, + &self, config, selection, cursor, @@ -1212,8 +1211,6 @@ impl Term { return; } - self.reset_url_highlight(); - let old_cols = self.grid.num_cols(); let old_lines = self.grid.num_lines(); let mut num_cols = size.cols(); @@ -1232,6 +1229,7 @@ impl Term { self.grid.selection = None; self.alt_grid.selection = None; + self.grid.url_highlight = None; // Should not allow less than 1 col, causes all sorts of checks to be required. if num_cols <= Column(1) { @@ -1381,37 +1379,22 @@ impl Term { } #[inline] - pub fn set_url_highlight(&mut self, hover_save: UrlHoverSaveState) { - self.url_hover_save = Some(hover_save); + pub fn set_url_highlight(&mut self, hl: RangeInclusive) { + self.grid.url_highlight = Some(hl); } #[inline] - pub fn url_highlight_start(&self) -> Option> { - self.url_hover_save.as_ref().map(|hs| hs.start) - } - - /// Remove the URL highlight and restore the previous mouse cursor and underline state. pub fn reset_url_highlight(&mut self) { - let hover_save = match self.url_hover_save.take() { - Some(hover_save) => hover_save, - _ => return, + let mouse_mode = + TermMode::MOUSE_MOTION | TermMode::MOUSE_DRAG | TermMode::MOUSE_REPORT_CLICK; + let mouse_cursor = if self.mode().intersects(mouse_mode) { + MouseCursor::Default + } else { + MouseCursor::Text }; + self.set_mouse_cursor(mouse_cursor); - // Reset the mouse cursor - self.set_mouse_cursor(hover_save.mouse_cursor); - - let last_line = self.size_info.lines().0 - 1; - let last_col = self.size_info.cols() - 1; - - // Reset the underline state after unhovering a URL - let mut iter = once(hover_save.start).chain(hover_save.start.iter(last_col, last_line)); - for underlined in &hover_save.underlined { - if let (Some(point), false) = (iter.next(), underlined) { - let cell = &mut self.grid[point.line][point.col]; - cell.flags.remove(Flags::UNDERLINE); - } - } - + self.grid.url_highlight = None; self.dirty = true; } } @@ -1961,8 +1944,9 @@ impl ansi::Handler for Term { let mut template = self.cursor.template; template.flags ^= template.flags; - // Remove active selections + // Remove active selections and URL highlights self.grid.selection = None; + self.grid.url_highlight = None; match mode { ansi::ClearMode::Below => { diff --git a/src/url.rs b/src/url.rs index 5920749..3366519 100644 --- a/src/url.rs +++ b/src/url.rs @@ -13,6 +13,7 @@ // limitations under the License. use url; +use unicode_width::UnicodeWidthChar; use crate::term::cell::{Cell, Flags}; @@ -28,14 +29,12 @@ const URL_SCHEMES: [&str; 8] = [ pub struct Url { pub text: String, pub origin: usize, - pub len: usize, } /// Parser for streaming inside-out detection of URLs. pub struct UrlParser { state: String, origin: usize, - len: usize, } impl UrlParser { @@ -43,7 +42,6 @@ impl UrlParser { UrlParser { state: String::new(), origin: 0, - len: 0, } } @@ -51,7 +49,6 @@ impl UrlParser { pub fn advance_left(&mut self, cell: &Cell) -> bool { if cell.flags.contains(Flags::WIDE_CHAR_SPACER) { self.origin += 1; - self.len += 1; return false; } @@ -59,7 +56,6 @@ impl UrlParser { true } else { self.origin += 1; - self.len += 1; false } } @@ -67,16 +63,10 @@ impl UrlParser { /// Advance the parser one character to the right. pub fn advance_right(&mut self, cell: &Cell) -> bool { if cell.flags.contains(Flags::WIDE_CHAR_SPACER) { - self.len += 1; return false; } - if self.advance(cell.c, self.state.len()) { - true - } else { - self.len += 1; - false - } + self.advance(cell.c, self.state.len()) } /// Returns the URL if the parser has found any. @@ -93,7 +83,7 @@ impl UrlParser { match c { 'a'...'z' | 'A'...'Z' => (), _ => { - self.origin = self.origin.saturating_sub(byte_index + 1); + self.origin = self.origin.saturating_sub(byte_index + c.width().unwrap_or(1)); self.state = self.state.split_off(byte_index + c.len_utf8()); break; } @@ -104,7 +94,7 @@ impl UrlParser { // Remove non-matching parenthesis and brackets let mut open_parens_count: isize = 0; let mut open_bracks_count: isize = 0; - for (i, c) in self.state.chars().enumerate() { + for (i, c) in self.state.char_indices() { match c { '(' => open_parens_count += 1, ')' if open_parens_count > 0 => open_parens_count -= 1, @@ -140,7 +130,6 @@ impl UrlParser { Some(Url { origin: self.origin - 1, text: self.state, - len: self.len, }) } else { None @@ -247,25 +236,14 @@ mod tests { let term = url_create_term("https://全.org"); let url = term.url_search(Point::new(0, Column(9))); assert_eq!(url.map(|u| u.origin), Some(9)); - } - #[test] - fn url_len() { - let term = url_create_term(" test https://example.org "); - let url = term.url_search(Point::new(0, Column(10))); - assert_eq!(url.map(|u| u.len), Some(19)); - - let term = url_create_term("https://全.org"); - let url = term.url_search(Point::new(0, Column(0))); - assert_eq!(url.map(|u| u.len), Some(14)); - - let term = url_create_term("https://全.org"); - let url = term.url_search(Point::new(0, Column(10))); - assert_eq!(url.map(|u| u.len), Some(14)); - - let term = url_create_term("https://全.org"); + let term = url_create_term("test@https://example.org"); let url = term.url_search(Point::new(0, Column(9))); - assert_eq!(url.map(|u| u.len), Some(14)); + assert_eq!(url.map(|u| u.origin), Some(4)); + + let term = url_create_term("test全https://example.org"); + let url = term.url_search(Point::new(0, Column(9))); + assert_eq!(url.map(|u| u.origin), Some(3)); } #[test] @@ -288,6 +266,8 @@ mod tests { url_test("'https://example.org'", "https://example.org"); url_test("'https://example.org", "https://example.org"); url_test("https://example.org'", "https://example.org"); + + url_test("(https://example.org/test全)", "https://example.org/test全"); } #[test]