Add a config option for navigate_incdec

Also known as Ctrl-A/Ctrl-X. You can now specify which parts of the URL
should be searched for numbers.

The setting is general->url-incdec-segments and it's a set with valid
values of 'host', 'path', 'query' and 'anchor'.
This commit is contained in:
Daniel 2015-09-30 14:17:07 +02:00
parent eb662a2468
commit 1bdb012b2c
6 changed files with 157 additions and 35 deletions

View File

@ -471,8 +471,9 @@ class CommandDispatcher:
background: Open the link in a new background tab.
window: Open the link in a new window.
"""
segments = config.get('general', 'url-incdec-segments')
try:
new_url = urlutils.incdec_number(url, incdec)
new_url = urlutils.incdec_number(url, incdec, segments=segments)
except urlutils.IncDecError as error:
raise cmdexc.CommandError(error.msg)
self._open(new_url, tab, background, window)

View File

@ -225,6 +225,11 @@ def data(readonly=False):
"The name of the session to save by default, or empty for the "
"last loaded session."),
('url-incdec-segments',
SettingValue(typ.URLSegmentList(none_ok=True), 'path,query'),
"The URL segments where `:navigate increment/decrement` will "
"search for a number."),
readonly=readonly
)),

View File

@ -1578,3 +1578,37 @@ class TabBarShow(BaseType):
"is open."),
('switching', "Show the tab bar when switching "
"tabs."))
class URLSegmentList(List):
"""A list of URL segments."""
valid_values = ValidValues('host', 'path', 'query', 'anchor')
def transform(self, value):
values = super().transform(value)
if values is None:
return set()
return set(values)
def validate(self, value):
self._basic_validation(value)
segments = super().transform(value)
if segments is None:
# Basic validation already checked if none_ok is True
return
faulty_segments = set(segments) - set(self.valid_values.values)
# faulty_segments is a set of options that are given but not valid
if None in faulty_segments:
raise configexc.ValidationError(value,
"Empty list item is not allowed")
if faulty_segments:
error_str = ("List contains invalid segments: {}"
.format(', '.join(faulty_segments)))
raise configexc.ValidationError(value, error_str)
# Make sure there are no duplicates
if len(set(segments)) != len(segments):
raise configexc.ValidationError(value,
"List contains duplicate segments")

View File

@ -456,12 +456,15 @@ class IncDecError(Exception):
return '{}: {}'.format(self.msg, self.url.toString())
def incdec_number(url, incdec):
def incdec_number(url, incdec, segments=None):
"""Find a number in the url and increment or decrement it.
Args:
url: The current url
incdec: Either 'increment' or 'decrement'
segments: A set of URL segments to search. Valid segments are:
'host', 'path', 'query', 'anchor'.
Default: {'path', 'query'}
Return:
The new url with the number incremented/decremented.
@ -471,24 +474,46 @@ def incdec_number(url, incdec):
if not url.isValid():
raise InvalidUrlError(url)
path = url.path()
# Get the last number in a string
match = re.match(r'(.*\D|^)(\d+)(.*)', path)
if not match:
raise IncDecError("No number found in URL!", url)
pre, number, post = match.groups()
# This should always succeed because we match \d+
val = int(number)
if incdec == 'decrement':
if val <= 0:
raise IncDecError("Can't decrement {}!".format(val), url)
val -= 1
elif incdec == 'increment':
val += 1
else:
raise ValueError("Invalid value {} for indec!".format(incdec))
new_path = ''.join([pre, str(val), post])
if segments is None:
segments = {'path', 'query'}
valid_segments = {'host', 'path', 'query', 'anchor'}
if segments - valid_segments:
extra_elements = segments - valid_segments
raise IncDecError("Invalid segments: {}".format(
', '.join(extra_elements)), url)
# Make a copy of the QUrl so we don't modify the original
new_url = QUrl(url)
new_url.setPath(new_path)
return new_url
url = QUrl(url)
# Order as they appear in a URL
segment_modifiers = [
('host', url.host, url.setHost),
('path', url.path, url.setPath),
('query', url.query, url.setQuery),
('anchor', url.fragment, url.setFragment),
]
# We're searching the last number so we walk the url segments backwards
for segment, getter, setter in reversed(segment_modifiers):
if segment not in segments:
continue
# Get the last number in a string
match = re.match(r'(.*\D|^)(\d+)(.*)', getter())
if not match:
continue
pre, number, post = match.groups()
# This should always succeed because we match \d+
val = int(number)
if incdec == 'decrement':
if val <= 0:
raise IncDecError("Can't decrement {}!".format(val), url)
val -= 1
elif incdec == 'increment':
val += 1
else:
raise ValueError("Invalid value {} for indec!".format(incdec))
new_value = ''.join([pre, str(val), post])
setter(new_value)
return url
raise IncDecError("No number found in URL!", url)

View File

@ -1902,6 +1902,43 @@ class TestUserAgent:
"""Simple smoke test for completion."""
klass().complete()
class TestURLSegmentList:
"""Test URLSegmentList."""
@pytest.fixture
def klass(self):
return configtypes.URLSegmentList
@pytest.mark.parametrize('val', [
'', 'host', 'path', 'query', 'anchor', 'host,path,anchor',
])
def test_validate_valid(self, klass, val):
klass(none_ok=True).validate(val)
def test_validate_empty(self, klass):
with pytest.raises(configexc.ValidationError):
klass(none_ok=False).validate('')
@pytest.mark.parametrize('val', [
'foo', 'bar', 'foo,bar', 'host,path,foo', 'host,host'
])
def test_validate_invalid(self, klass, val):
with pytest.raises(configexc.ValidationError):
klass(none_ok=True).validate(val)
@pytest.mark.parametrize('val, expected', [
('', set()),
('path', {'path'}),
('path,query', {'path', 'query'}),
])
def test_transform(self, klass, val, expected):
assert klass().transform(val) == expected
def test_complete(self, klass):
"""Simple smoke test for completion."""
klass().complete()
@pytest.mark.parametrize('first, second, equal', [
(re.compile('foo'), RegexEq('foo'), True),

View File

@ -531,22 +531,36 @@ class TestIncDecNumber:
"""Tests for urlutils.incdec_number()."""
@pytest.mark.parametrize('url, incdec, output', [
("http://example.com/index1.html", "increment", "http://example.com/index2.html"),
("http://foo.bar/folder_1/image_2", "increment", "http://foo.bar/folder_1/image_3"),
("http://bbc.c0.uk:80/story_1", "increment", "http://bbc.c0.uk:80/story_2"),
("http://mydomain.tld/1_%C3%A4", "increment", "http://mydomain.tld/2_%C3%A4"),
("http://example.com/site/5#5", "increment", "http://example.com/site/6#5"),
@pytest.mark.parametrize('url, incdec, output, segments', [
("http://example.com/index1.html", "increment",
"http://example.com/index2.html", {'path'}),
("http://foo.bar/folder_1/image_2", "increment",
"http://foo.bar/folder_1/image_3", {'path'}),
("http://bbc.c0.uk:80/story_1", "increment",
"http://bbc.c0.uk:80/story_2", {'path'}),
("http://mydomain.tld/1_%C3%A4", "increment",
"http://mydomain.tld/2_%C3%A4", {'path'}),
("http://example.com/site/5#5", "increment",
"http://example.com/site/6#5", {'path'}),
("http://example.com/site/1#1", "increment",
"http://example.com/site/1#2", {'path', 'anchor'}),
("http://example.com/site/1?page=2#3", "increment",
"http://example.com/site/1?page=3#3", {'path', 'query'}),
("http://example.com/index10.html", "decrement", "http://example.com/index9.html"),
("http://foo.bar/folder_1/image_3", "decrement", "http://foo.bar/folder_1/image_2"),
("http://bbc.c0.uk:80/story_1", "decrement", "http://bbc.c0.uk:80/story_0"),
("http://mydomain.tld/2_%C3%A4", "decrement", "http://mydomain.tld/1_%C3%A4"),
("http://example.com/site/5#5", "decrement", "http://example.com/site/4#5"),
("http://example.com/index10.html", "decrement",
"http://example.com/index9.html", {'path'}),
("http://foo.bar/folder_1/image_3", "decrement",
"http://foo.bar/folder_1/image_2", {'path'}),
("http://bbc.c0.uk:80/story_1", "decrement",
"http://bbc.c0.uk:80/story_0", {'path'}),
("http://mydomain.tld/2_%C3%A4", "decrement",
"http://mydomain.tld/1_%C3%A4", {'path'}),
("http://example.com/site/5#5", "decrement",
"http://example.com/site/4#5", {'path'}),
])
def test_incdec_number(self, url, incdec, output):
def test_incdec_number(self, url, incdec, output, segments):
"""Test incdec_number with valid URLs."""
new_url = urlutils.incdec_number(QUrl(url), incdec)
new_url = urlutils.incdec_number(QUrl(url), incdec, segments=segments)
assert new_url == QUrl(output)
@pytest.mark.parametrize('url', [
@ -582,6 +596,12 @@ class TestIncDecNumber:
with pytest.raises(ValueError):
urlutils.incdec_number(valid_url, "foobar")
def test_wrong_segment(self):
"""Test if incdec_number rejects a wrong segment"""
with pytest.raises(urlutils.IncDecError):
urlutils.incdec_number(QUrl('http://example.com'),
'increment', segments={'foobar'})
@pytest.mark.parametrize("url, msg, expected_str", [
("http://example.com", "Invalid", "Invalid: http://example.com"),
])