From 668942095e15a94c08798e2ddf9a28417bf4ecf6 Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Thu, 29 Jun 2023 22:34:43 -0400 Subject: [PATCH 01/27] add .venv to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index cb4a5e0a5..553779059 100644 --- a/.gitignore +++ b/.gitignore @@ -79,6 +79,7 @@ celerybeat-schedule .env # virtualenv +.venv/ venv/ ENV/ env*/ From fa712b7b361c53eead74e36ba77b0fb363c41212 Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Fri, 30 Jun 2023 19:02:56 -0400 Subject: [PATCH 02/27] markdown plugin --- porcupine/plugins/markdown.py | 56 +++++++++++ tests/test_markdown_plugin.py | 183 ++++++++++++++++++++++++++++++++++ 2 files changed, 239 insertions(+) create mode 100644 porcupine/plugins/markdown.py create mode 100644 tests/test_markdown_plugin.py diff --git a/porcupine/plugins/markdown.py b/porcupine/plugins/markdown.py new file mode 100644 index 000000000..f51fe3cf8 --- /dev/null +++ b/porcupine/plugins/markdown.py @@ -0,0 +1,56 @@ +"""If configuration says so, insert spaces when the tab key is pressed.""" + +from __future__ import annotations + +import logging +import re +import tkinter +from functools import partial + +from porcupine import get_tab_manager, tabs, textutils, utils + +log = logging.getLogger(__name__) + + +setup_before = ["tabs2spaces"] + + +def _is_list_item(line: str) -> bool: + """Detect if the line that is passed is a markdown list item + + According to: + - https://spec.commonmark.org/0.30/#lists + - https://pandoc.org/MANUAL.html#lists + """ + assert len(line.splitlines()) == 1 + pattern = r"^\s*\d{1,9}[.)]|^\s*[-+*]|^\s*#\)|^\s*#\." + regex = re.compile(pattern) + match = regex.search(line) + return bool(match) + + +def on_tab_key( + tab: tabs.FileTab, + event: tkinter.Event[textutils.MainText], + shift_pressed: bool, +) -> str: + """Indenting and dedenting list items""" + if tab.settings.get("filetype_name", str) == "Markdown": + line = event.widget.get("insert linestart", "insert lineend") + list_item_status = _is_list_item(line) + + if shift_pressed and list_item_status: + event.widget.dedent("insert linestart") + return "break" + + if list_item_status: + event.widget.indent("insert linestart") + return "break" + + +def on_new_filetab(tab: tabs.FileTab) -> None: + utils.bind_tab_key(tab.textwidget, partial(on_tab_key, tab), add=True) + + +def setup() -> None: + get_tab_manager().add_filetab_callback(on_new_filetab) diff --git a/tests/test_markdown_plugin.py b/tests/test_markdown_plugin.py new file mode 100644 index 000000000..d5d8cce0e --- /dev/null +++ b/tests/test_markdown_plugin.py @@ -0,0 +1,183 @@ +from __future__ import annotations + +import itertools +from contextlib import nullcontext as does_not_raise +from typing import NamedTuple + +import pytest + +from porcupine.plugins import markdown + + +class IsListItemCase(NamedTuple): + id: str + line: str + expected: bool + marks: list[pytest.MarkDecorator] = [] + raises: Exception | None = None + + +IS_LIST_ITEM_CASES = [ + IsListItemCase(id="# with no separator", line="# item 1", expected=False), + IsListItemCase(id="# bad separator |", line="#| item 1", expected=False), + IsListItemCase(id="# bad separator /", line="#/ item 1", expected=False), + IsListItemCase(id="# bad separator \\", line="#\\ item 1", expected=False), + IsListItemCase(id="ol bad separator |", line="8| item 1", expected=False), + IsListItemCase(id="ol bad separator /", line="8/ item 1", expected=False), + IsListItemCase( + id="ol bad separator \\", line="8\\ item 1", expected=False + ), + IsListItemCase(id="not a list 1", line="item 1", expected=False), + IsListItemCase(id="not a list 2", line=" item 1", expected=False), + IsListItemCase(id="not a list 3", line=" item 1", expected=False), + IsListItemCase(id="not a list 4", line="& item 1", expected=False), + IsListItemCase(id="not a list 5", line="^ item 1", expected=False), + IsListItemCase(id="duplicate token 1", line="-- item 1", expected=True), + IsListItemCase(id="duplicate token 2", line="--- item 1", expected=True), + IsListItemCase(id="duplicate token 3", line="- - - item 1", expected=True), + IsListItemCase( + id="duplicate token 4", line=" - item -- 1 -", expected=True + ), + IsListItemCase( + id="duplicate token 5", line=" -#) item -- 1 -", expected=True + ), + IsListItemCase( + id="duplicate token 6", line=" *-#)1. item -- 1 -", expected=True + ), +] + +# test `#` and 0 to 99 numbered lists +# tests ol with `.` and `)` +IS_LIST_ITEM_CASES.extend( + [ + IsListItemCase( + id=f"numbered {i}", line=f"{i}{sep} item 1", expected=True + ) + for i, sep in itertools.product( + itertools.chain(range(100), "#"), (".", ")") + ) + ] +) + +# test numbered list with whitespace following and preceding +IS_LIST_ITEM_CASES.extend( + [ + IsListItemCase( + id=f"numbered {preceding=} {following=} space", + line=f"{' ' * preceding}{i}{sep}{' ' * following} item 1", + expected=True, + ) + for i, sep, preceding, following in itertools.product( + ("7", "#"), (".", ")"), range(11), range(11) + ) + ] +) + +# test with whitespace following and preceding +IS_LIST_ITEM_CASES.extend( + [ + IsListItemCase( + id=f"bullet {preceding=} {following=} space", + line=f"{' ' * preceding}{bullet} {' ' * following} item 1", + expected=True, + ) + for bullet, preceding, following in itertools.product( + ("-", "*", "+"), range(11), range(11) + ) + ] +) + + +@pytest.mark.parametrize( + "line, expected, raises", + [ + pytest.param( + case.line, + case.expected, + pytest.raises(case.raises) if case.raises else does_not_raise(), + marks=case.marks, + id=case.id, + ) + for case in IS_LIST_ITEM_CASES + ], +) +def test_is_list(line: str, expected: bool, raises): + with raises: + assert markdown._is_list_item(line) == expected + + +@pytest.mark.parametrize( + "li", + [ + "1. item 1", + "1) item 1", + "#) item 1", + "- item 1", + "* item 1", + "+ item 1", + "++++++ weird", + "1)))))) still weird", + "- [ ] unchecked task", + "- [X] checked task", + ], +) +def test_filetype_switching(li: str, filetab, tmp_path): + assert filetab.settings.get("filetype_name", object) == "Python" + + filetab.textwidget.insert("1.0", li) + filetab.textwidget.event_generate("") + filetab.update() + assert ( + filetab.textwidget.get("1.0", "end - 1 char") == li + ), "should not effect list items unless using markdown filetype" + filetab.textwidget.event_generate("") # close the autocomplete + + # switch to Markdown filetype format + filetab.save_as(tmp_path / "asdf.md") + assert filetab.settings.get("filetype_name", object) == "Markdown" + + filetab.textwidget.event_generate("") + filetab.update() + assert ( + filetab.textwidget.get("1.0", "end - 1 char") == f" {li}\n" + ), "should indent" + filetab.textwidget.event_generate("") + filetab.update() + assert ( + filetab.textwidget.get("1.0", "end - 1 char") == f"{li}\n" + ), "should dedent" + + +@pytest.mark.parametrize( + "line", + [ + "# H1 Heading", + "## H2 Heading", + " ### H3 Heading with whitespace preceding", + "| Markdown | Table |", + "| :---------------- | :------: | ----: |", + "```python", + '

My Great Heading

', + ": This is the definition", + "~~The world is flat.~~", + "==very important words==", + "X^2^", + "http://www.example.com", + "`http://www.example.com`", + ], +) +def test_non_list(line: str, filetab, tmp_path): + import time + + # switch to Markdown filetype format + filetab.save_as(tmp_path / "asdf.md") + assert filetab.settings.get("filetype_name", object) == "Markdown" + + filetab.textwidget.insert("1.0", line) + filetab.textwidget.event_generate("") + filetab.textwidget.event_generate("") # close the autocomplete + filetab.update() + # time.sleep(3) + assert ( + filetab.textwidget.get("1.0", "end - 1 char") == f"{line}\n" + ), "should not change, just open autocomplete" From 2079603aeea217e133fd34046b1babf73b1c3c29 Mon Sep 17 00:00:00 2001 From: benjamin-kirkbride Date: Fri, 30 Jun 2023 23:10:47 +0000 Subject: [PATCH 03/27] Run pycln, black and isort --- porcupine/plugins/markdown.py | 4 +--- tests/test_markdown_plugin.py | 38 +++++++++-------------------------- 2 files changed, 10 insertions(+), 32 deletions(-) diff --git a/porcupine/plugins/markdown.py b/porcupine/plugins/markdown.py index f51fe3cf8..446f4dbe3 100644 --- a/porcupine/plugins/markdown.py +++ b/porcupine/plugins/markdown.py @@ -30,9 +30,7 @@ def _is_list_item(line: str) -> bool: def on_tab_key( - tab: tabs.FileTab, - event: tkinter.Event[textutils.MainText], - shift_pressed: bool, + tab: tabs.FileTab, event: tkinter.Event[textutils.MainText], shift_pressed: bool ) -> str: """Indenting and dedenting list items""" if tab.settings.get("filetype_name", str) == "Markdown": diff --git a/tests/test_markdown_plugin.py b/tests/test_markdown_plugin.py index d5d8cce0e..4d3c6de01 100644 --- a/tests/test_markdown_plugin.py +++ b/tests/test_markdown_plugin.py @@ -24,9 +24,7 @@ class IsListItemCase(NamedTuple): IsListItemCase(id="# bad separator \\", line="#\\ item 1", expected=False), IsListItemCase(id="ol bad separator |", line="8| item 1", expected=False), IsListItemCase(id="ol bad separator /", line="8/ item 1", expected=False), - IsListItemCase( - id="ol bad separator \\", line="8\\ item 1", expected=False - ), + IsListItemCase(id="ol bad separator \\", line="8\\ item 1", expected=False), IsListItemCase(id="not a list 1", line="item 1", expected=False), IsListItemCase(id="not a list 2", line=" item 1", expected=False), IsListItemCase(id="not a list 3", line=" item 1", expected=False), @@ -35,27 +33,17 @@ class IsListItemCase(NamedTuple): IsListItemCase(id="duplicate token 1", line="-- item 1", expected=True), IsListItemCase(id="duplicate token 2", line="--- item 1", expected=True), IsListItemCase(id="duplicate token 3", line="- - - item 1", expected=True), - IsListItemCase( - id="duplicate token 4", line=" - item -- 1 -", expected=True - ), - IsListItemCase( - id="duplicate token 5", line=" -#) item -- 1 -", expected=True - ), - IsListItemCase( - id="duplicate token 6", line=" *-#)1. item -- 1 -", expected=True - ), + IsListItemCase(id="duplicate token 4", line=" - item -- 1 -", expected=True), + IsListItemCase(id="duplicate token 5", line=" -#) item -- 1 -", expected=True), + IsListItemCase(id="duplicate token 6", line=" *-#)1. item -- 1 -", expected=True), ] # test `#` and 0 to 99 numbered lists # tests ol with `.` and `)` IS_LIST_ITEM_CASES.extend( [ - IsListItemCase( - id=f"numbered {i}", line=f"{i}{sep} item 1", expected=True - ) - for i, sep in itertools.product( - itertools.chain(range(100), "#"), (".", ")") - ) + IsListItemCase(id=f"numbered {i}", line=f"{i}{sep} item 1", expected=True) + for i, sep in itertools.product(itertools.chain(range(100), "#"), (".", ")")) ] ) @@ -81,9 +69,7 @@ class IsListItemCase(NamedTuple): line=f"{' ' * preceding}{bullet} {' ' * following} item 1", expected=True, ) - for bullet, preceding, following in itertools.product( - ("-", "*", "+"), range(11), range(11) - ) + for bullet, preceding, following in itertools.product(("-", "*", "+"), range(11), range(11)) ] ) @@ -138,14 +124,10 @@ def test_filetype_switching(li: str, filetab, tmp_path): filetab.textwidget.event_generate("") filetab.update() - assert ( - filetab.textwidget.get("1.0", "end - 1 char") == f" {li}\n" - ), "should indent" + assert filetab.textwidget.get("1.0", "end - 1 char") == f" {li}\n", "should indent" filetab.textwidget.event_generate("") filetab.update() - assert ( - filetab.textwidget.get("1.0", "end - 1 char") == f"{li}\n" - ), "should dedent" + assert filetab.textwidget.get("1.0", "end - 1 char") == f"{li}\n", "should dedent" @pytest.mark.parametrize( @@ -167,8 +149,6 @@ def test_filetype_switching(li: str, filetab, tmp_path): ], ) def test_non_list(line: str, filetab, tmp_path): - import time - # switch to Markdown filetype format filetab.save_as(tmp_path / "asdf.md") assert filetab.settings.get("filetype_name", object) == "Markdown" From fb1a190da968c55178788769ff6a2d543c1e6b51 Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Fri, 30 Jun 2023 21:04:26 -0400 Subject: [PATCH 04/27] add explicit return --- porcupine/plugins/markdown.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/porcupine/plugins/markdown.py b/porcupine/plugins/markdown.py index 446f4dbe3..2dcba8362 100644 --- a/porcupine/plugins/markdown.py +++ b/porcupine/plugins/markdown.py @@ -30,8 +30,10 @@ def _is_list_item(line: str) -> bool: def on_tab_key( - tab: tabs.FileTab, event: tkinter.Event[textutils.MainText], shift_pressed: bool -) -> str: + tab: tabs.FileTab, + event: tkinter.Event[textutils.MainText], + shift_pressed: bool, +) -> str | None: """Indenting and dedenting list items""" if tab.settings.get("filetype_name", str) == "Markdown": line = event.widget.get("insert linestart", "insert lineend") @@ -45,6 +47,8 @@ def on_tab_key( event.widget.indent("insert linestart") return "break" + return None + def on_new_filetab(tab: tabs.FileTab) -> None: utils.bind_tab_key(tab.textwidget, partial(on_tab_key, tab), add=True) From cd3841e6dda011c41fef36ece0b555f31d7c4d7d Mon Sep 17 00:00:00 2001 From: benjamin-kirkbride Date: Sat, 1 Jul 2023 01:06:21 +0000 Subject: [PATCH 05/27] Run pycln, black and isort --- porcupine/plugins/markdown.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/porcupine/plugins/markdown.py b/porcupine/plugins/markdown.py index 2dcba8362..271b582e1 100644 --- a/porcupine/plugins/markdown.py +++ b/porcupine/plugins/markdown.py @@ -30,9 +30,7 @@ def _is_list_item(line: str) -> bool: def on_tab_key( - tab: tabs.FileTab, - event: tkinter.Event[textutils.MainText], - shift_pressed: bool, + tab: tabs.FileTab, event: tkinter.Event[textutils.MainText], shift_pressed: bool ) -> str | None: """Indenting and dedenting list items""" if tab.settings.get("filetype_name", str) == "Markdown": From 98d3497f4fdc4165a32d84e3bdc2e97b8696bb2f Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Fri, 30 Jun 2023 21:23:31 -0400 Subject: [PATCH 06/27] improve coverage and regex --- porcupine/plugins/markdown.py | 2 +- tests/test_markdown_plugin.py | 58 +++++++++++++++++++++++++++-------- 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/porcupine/plugins/markdown.py b/porcupine/plugins/markdown.py index 271b582e1..5bee4f566 100644 --- a/porcupine/plugins/markdown.py +++ b/porcupine/plugins/markdown.py @@ -23,7 +23,7 @@ def _is_list_item(line: str) -> bool: - https://pandoc.org/MANUAL.html#lists """ assert len(line.splitlines()) == 1 - pattern = r"^\s*\d{1,9}[.)]|^\s*[-+*]|^\s*#\)|^\s*#\." + pattern = r"(^\s*\d{1,9}[.)]|^\s*[-+*]|^\s*#\)|^\s*#\.) .*" regex = re.compile(pattern) match = regex.search(line) return bool(match) diff --git a/tests/test_markdown_plugin.py b/tests/test_markdown_plugin.py index 4d3c6de01..682c3e1ab 100644 --- a/tests/test_markdown_plugin.py +++ b/tests/test_markdown_plugin.py @@ -24,26 +24,52 @@ class IsListItemCase(NamedTuple): IsListItemCase(id="# bad separator \\", line="#\\ item 1", expected=False), IsListItemCase(id="ol bad separator |", line="8| item 1", expected=False), IsListItemCase(id="ol bad separator /", line="8/ item 1", expected=False), - IsListItemCase(id="ol bad separator \\", line="8\\ item 1", expected=False), + IsListItemCase( + id="ol bad separator \\", line="8\\ item 1", expected=False + ), IsListItemCase(id="not a list 1", line="item 1", expected=False), IsListItemCase(id="not a list 2", line=" item 1", expected=False), IsListItemCase(id="not a list 3", line=" item 1", expected=False), IsListItemCase(id="not a list 4", line="& item 1", expected=False), IsListItemCase(id="not a list 5", line="^ item 1", expected=False), - IsListItemCase(id="duplicate token 1", line="-- item 1", expected=True), - IsListItemCase(id="duplicate token 2", line="--- item 1", expected=True), + IsListItemCase(id="duplicate token 1", line="-- item 1", expected=False), + IsListItemCase(id="duplicate token 2", line="--- item 1", expected=False), IsListItemCase(id="duplicate token 3", line="- - - item 1", expected=True), - IsListItemCase(id="duplicate token 4", line=" - item -- 1 -", expected=True), - IsListItemCase(id="duplicate token 5", line=" -#) item -- 1 -", expected=True), - IsListItemCase(id="duplicate token 6", line=" *-#)1. item -- 1 -", expected=True), + IsListItemCase( + id="duplicate token 4", line=" - item -- 1 -", expected=True + ), + IsListItemCase( + id="duplicate token 5", line=" -#) item -- 1 -", expected=False + ), + IsListItemCase( + id="duplicate token 6", line=" *-#)1. item -- 1 -", expected=False + ), ] # test `#` and 0 to 99 numbered lists # tests ol with `.` and `)` IS_LIST_ITEM_CASES.extend( [ - IsListItemCase(id=f"numbered {i}", line=f"{i}{sep} item 1", expected=True) - for i, sep in itertools.product(itertools.chain(range(100), "#"), (".", ")")) + IsListItemCase( + id=f"numbered {i}", line=f"{i}{sep} item 1", expected=True + ) + for i, sep in itertools.product( + itertools.chain(range(100), "#"), (".", ")") + ) + ] +) + +# test raw li prefixes with and without space +IS_LIST_ITEM_CASES.extend( + [ + IsListItemCase( + id=f"raw prexix {prefix} no space", + line=f"{prefix}{' ' if space else ''}", + expected=space, + ) + for prefix, space in itertools.product( + ["1.", "1)", "#.", "#)", "-", "*", "+"], [True, False] + ) ] ) @@ -69,7 +95,9 @@ class IsListItemCase(NamedTuple): line=f"{' ' * preceding}{bullet} {' ' * following} item 1", expected=True, ) - for bullet, preceding, following in itertools.product(("-", "*", "+"), range(11), range(11)) + for bullet, preceding, following in itertools.product( + ("-", "*", "+"), range(11), range(11) + ) ] ) @@ -101,8 +129,8 @@ def test_is_list(line: str, expected: bool, raises): "- item 1", "* item 1", "+ item 1", - "++++++ weird", - "1)))))) still weird", + "+ +++++ weird", + "1) ))))) still weird", "- [ ] unchecked task", "- [X] checked task", ], @@ -124,10 +152,14 @@ def test_filetype_switching(li: str, filetab, tmp_path): filetab.textwidget.event_generate("") filetab.update() - assert filetab.textwidget.get("1.0", "end - 1 char") == f" {li}\n", "should indent" + assert ( + filetab.textwidget.get("1.0", "end - 1 char") == f" {li}\n" + ), "should indent" filetab.textwidget.event_generate("") filetab.update() - assert filetab.textwidget.get("1.0", "end - 1 char") == f"{li}\n", "should dedent" + assert ( + filetab.textwidget.get("1.0", "end - 1 char") == f"{li}\n" + ), "should dedent" @pytest.mark.parametrize( From 856d4b40ee1576ab83c80f8d7361219b3aee75b9 Mon Sep 17 00:00:00 2001 From: benjamin-kirkbride Date: Sat, 1 Jul 2023 01:24:11 +0000 Subject: [PATCH 07/27] Run pycln, black and isort --- tests/test_markdown_plugin.py | 36 +++++++++-------------------------- 1 file changed, 9 insertions(+), 27 deletions(-) diff --git a/tests/test_markdown_plugin.py b/tests/test_markdown_plugin.py index 682c3e1ab..222802859 100644 --- a/tests/test_markdown_plugin.py +++ b/tests/test_markdown_plugin.py @@ -24,9 +24,7 @@ class IsListItemCase(NamedTuple): IsListItemCase(id="# bad separator \\", line="#\\ item 1", expected=False), IsListItemCase(id="ol bad separator |", line="8| item 1", expected=False), IsListItemCase(id="ol bad separator /", line="8/ item 1", expected=False), - IsListItemCase( - id="ol bad separator \\", line="8\\ item 1", expected=False - ), + IsListItemCase(id="ol bad separator \\", line="8\\ item 1", expected=False), IsListItemCase(id="not a list 1", line="item 1", expected=False), IsListItemCase(id="not a list 2", line=" item 1", expected=False), IsListItemCase(id="not a list 3", line=" item 1", expected=False), @@ -35,27 +33,17 @@ class IsListItemCase(NamedTuple): IsListItemCase(id="duplicate token 1", line="-- item 1", expected=False), IsListItemCase(id="duplicate token 2", line="--- item 1", expected=False), IsListItemCase(id="duplicate token 3", line="- - - item 1", expected=True), - IsListItemCase( - id="duplicate token 4", line=" - item -- 1 -", expected=True - ), - IsListItemCase( - id="duplicate token 5", line=" -#) item -- 1 -", expected=False - ), - IsListItemCase( - id="duplicate token 6", line=" *-#)1. item -- 1 -", expected=False - ), + IsListItemCase(id="duplicate token 4", line=" - item -- 1 -", expected=True), + IsListItemCase(id="duplicate token 5", line=" -#) item -- 1 -", expected=False), + IsListItemCase(id="duplicate token 6", line=" *-#)1. item -- 1 -", expected=False), ] # test `#` and 0 to 99 numbered lists # tests ol with `.` and `)` IS_LIST_ITEM_CASES.extend( [ - IsListItemCase( - id=f"numbered {i}", line=f"{i}{sep} item 1", expected=True - ) - for i, sep in itertools.product( - itertools.chain(range(100), "#"), (".", ")") - ) + IsListItemCase(id=f"numbered {i}", line=f"{i}{sep} item 1", expected=True) + for i, sep in itertools.product(itertools.chain(range(100), "#"), (".", ")")) ] ) @@ -95,9 +83,7 @@ class IsListItemCase(NamedTuple): line=f"{' ' * preceding}{bullet} {' ' * following} item 1", expected=True, ) - for bullet, preceding, following in itertools.product( - ("-", "*", "+"), range(11), range(11) - ) + for bullet, preceding, following in itertools.product(("-", "*", "+"), range(11), range(11)) ] ) @@ -152,14 +138,10 @@ def test_filetype_switching(li: str, filetab, tmp_path): filetab.textwidget.event_generate("") filetab.update() - assert ( - filetab.textwidget.get("1.0", "end - 1 char") == f" {li}\n" - ), "should indent" + assert filetab.textwidget.get("1.0", "end - 1 char") == f" {li}\n", "should indent" filetab.textwidget.event_generate("") filetab.update() - assert ( - filetab.textwidget.get("1.0", "end - 1 char") == f"{li}\n" - ), "should dedent" + assert filetab.textwidget.get("1.0", "end - 1 char") == f"{li}\n", "should dedent" @pytest.mark.parametrize( From e32016cc61387ce4d3cfe7930c70483f32686481 Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Sat, 1 Jul 2023 16:56:00 -0400 Subject: [PATCH 08/27] remove autoindent regexes for markdown --- porcupine/default_filetypes.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/porcupine/default_filetypes.toml b/porcupine/default_filetypes.toml index 83631142a..b3bbaf0e3 100644 --- a/porcupine/default_filetypes.toml +++ b/porcupine/default_filetypes.toml @@ -377,7 +377,6 @@ filename_patterns = ["*.md", "*.markdown"] pygments_lexer = "pygments.lexers.MarkdownLexer" syntax_highlighter = "tree_sitter" tree_sitter_language_name = "markdown" -autoindent_regexes = {dedent = '.*\.', indent = '^([0-9]+\.|-) .*'} [YAML] filename_patterns = ["*.yml", "*.yaml"] From ca220ec135e4281bc41c017c5ad038966ce2cb40 Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Sat, 1 Jul 2023 16:56:28 -0400 Subject: [PATCH 09/27] remove extranious sleep --- tests/test_markdown_plugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_markdown_plugin.py b/tests/test_markdown_plugin.py index 222802859..89bb9851d 100644 --- a/tests/test_markdown_plugin.py +++ b/tests/test_markdown_plugin.py @@ -171,7 +171,6 @@ def test_non_list(line: str, filetab, tmp_path): filetab.textwidget.event_generate("") filetab.textwidget.event_generate("") # close the autocomplete filetab.update() - # time.sleep(3) assert ( filetab.textwidget.get("1.0", "end - 1 char") == f"{line}\n" ), "should not change, just open autocomplete" From 8b3e3df0586385815e483df3cd05a04f76809e65 Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Sat, 1 Jul 2023 16:58:35 -0400 Subject: [PATCH 10/27] update module docstring --- porcupine/plugins/markdown.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/porcupine/plugins/markdown.py b/porcupine/plugins/markdown.py index 5bee4f566..5f1f392ee 100644 --- a/porcupine/plugins/markdown.py +++ b/porcupine/plugins/markdown.py @@ -1,4 +1,7 @@ -"""If configuration says so, insert spaces when the tab key is pressed.""" +"""Features for working with Markdown Files. + +- Indenting and dedenting lists +""" from __future__ import annotations From a1479f6a9e0b3631fa10b946b5a12627a7fb9e71 Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Sat, 1 Jul 2023 17:55:11 -0400 Subject: [PATCH 11/27] fix autocomplete --- porcupine/plugins/markdown.py | 7 ++++++- tests/test_markdown_plugin.py | 16 +++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/porcupine/plugins/markdown.py b/porcupine/plugins/markdown.py index 5f1f392ee..7e5b61ef2 100644 --- a/porcupine/plugins/markdown.py +++ b/porcupine/plugins/markdown.py @@ -40,11 +40,16 @@ def on_tab_key( line = event.widget.get("insert linestart", "insert lineend") list_item_status = _is_list_item(line) + # shift-tab if shift_pressed and list_item_status: event.widget.dedent("insert linestart") return "break" - if list_item_status: + # if it isn't, we want tab to trigger autocomplete instead + char_before_cursor_is_space = tab.textwidget.get("insert - 1 char", "insert") == " " + + # tab + if list_item_status and char_before_cursor_is_space: event.widget.indent("insert linestart") return "break" diff --git a/tests/test_markdown_plugin.py b/tests/test_markdown_plugin.py index 89bb9851d..dfe226730 100644 --- a/tests/test_markdown_plugin.py +++ b/tests/test_markdown_plugin.py @@ -109,6 +109,8 @@ def test_is_list(line: str, expected: bool, raises): @pytest.mark.parametrize( "li", [ + "-", + "1.", "1. item 1", "1) item 1", "#) item 1", @@ -127,8 +129,9 @@ def test_filetype_switching(li: str, filetab, tmp_path): filetab.textwidget.insert("1.0", li) filetab.textwidget.event_generate("") filetab.update() + assert ( - filetab.textwidget.get("1.0", "end - 1 char") == li + filetab.textwidget.get("1.0", "insert") == li ), "should not effect list items unless using markdown filetype" filetab.textwidget.event_generate("") # close the autocomplete @@ -138,10 +141,17 @@ def test_filetype_switching(li: str, filetab, tmp_path): filetab.textwidget.event_generate("") filetab.update() - assert filetab.textwidget.get("1.0", "end - 1 char") == f" {li}\n", "should indent" + # no change to text, should open autocomplete menu + assert filetab.textwidget.get("1.0", "insert") == li + filetab.textwidget.event_generate("") # close the autocomplete + + # add a space + filetab.textwidget.insert("insert", " ") + filetab.textwidget.event_generate("") + assert filetab.textwidget.get("1.0", "insert") == f" {li} ", "should be indented" filetab.textwidget.event_generate("") filetab.update() - assert filetab.textwidget.get("1.0", "end - 1 char") == f"{li}\n", "should dedent" + assert filetab.textwidget.get("1.0", "insert") == f"{li} ", "should be back to normal" @pytest.mark.parametrize( From c27e78210d3754e4faab97125f89fef03a8793dd Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Sat, 1 Jul 2023 22:01:32 -0400 Subject: [PATCH 12/27] fix assert --- porcupine/plugins/markdown.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/porcupine/plugins/markdown.py b/porcupine/plugins/markdown.py index 7e5b61ef2..01a1e8697 100644 --- a/porcupine/plugins/markdown.py +++ b/porcupine/plugins/markdown.py @@ -25,7 +25,13 @@ def _is_list_item(line: str) -> bool: - https://spec.commonmark.org/0.30/#lists - https://pandoc.org/MANUAL.html#lists """ + assert isinstance(line, str) + if not line: + # empty string + return False + assert len(line.splitlines()) == 1 + pattern = r"(^\s*\d{1,9}[.)]|^\s*[-+*]|^\s*#\)|^\s*#\.) .*" regex = re.compile(pattern) match = regex.search(line) From 3a8aee79cef926f31a80f4671d3089c372981810 Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Sat, 1 Jul 2023 22:05:00 -0400 Subject: [PATCH 13/27] fix flaking (?) tests --- tests/test_markdown_plugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_markdown_plugin.py b/tests/test_markdown_plugin.py index dfe226730..4e5f8d9fb 100644 --- a/tests/test_markdown_plugin.py +++ b/tests/test_markdown_plugin.py @@ -148,6 +148,7 @@ def test_filetype_switching(li: str, filetab, tmp_path): # add a space filetab.textwidget.insert("insert", " ") filetab.textwidget.event_generate("") + filetab.update() assert filetab.textwidget.get("1.0", "insert") == f" {li} ", "should be indented" filetab.textwidget.event_generate("") filetab.update() From 225732bf7f8ac0e8f67bae69198b779d73dbc2d7 Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Sun, 2 Jul 2023 21:04:17 -0400 Subject: [PATCH 14/27] add list continuation --- porcupine/plugins/autoindent.py | 2 + porcupine/plugins/markdown.py | 38 +++++++++++---- tests/test_markdown_plugin.py | 84 +++++++++++++++++++++++---------- 3 files changed, 92 insertions(+), 32 deletions(-) diff --git a/porcupine/plugins/autoindent.py b/porcupine/plugins/autoindent.py index 34ec70baf..4d67fb325 100644 --- a/porcupine/plugins/autoindent.py +++ b/porcupine/plugins/autoindent.py @@ -90,6 +90,8 @@ def after_enter(tab: tabs.FileTab, alt_pressed: bool) -> None: if dedent_prev_line: tab.textwidget.dedent("insert - 1 line") + tab.textwidget.event_generate("<>") + def on_enter_press( tab: tabs.FileTab, alt_pressed: bool, event: tkinter.Event[tkinter.Text] diff --git a/porcupine/plugins/markdown.py b/porcupine/plugins/markdown.py index 01a1e8697..a73b3c5fc 100644 --- a/porcupine/plugins/markdown.py +++ b/porcupine/plugins/markdown.py @@ -18,24 +18,28 @@ setup_before = ["tabs2spaces"] -def _is_list_item(line: str) -> bool: - """Detect if the line that is passed is a markdown list item +def _list_item(line: str) -> re.Match[str] | None: + """Regex for markdown list item + + First group is the whitespace (if any) preceding the item + Second group is the list item prefix (ex `-`, `+`, `6.`, `#.`) According to: - https://spec.commonmark.org/0.30/#lists - https://pandoc.org/MANUAL.html#lists + Technically `#)` is not in either spec, but I won't tell if you won't """ + print(f"{line=}") assert isinstance(line, str) if not line: # empty string - return False + return None assert len(line.splitlines()) == 1 - pattern = r"(^\s*\d{1,9}[.)]|^\s*[-+*]|^\s*#\)|^\s*#\.) .*" - regex = re.compile(pattern) - match = regex.search(line) - return bool(match) + list_item_regex = re.compile(r"(^[\t ]*)(\d{1,9}[.)]|[-+*]|#\)|#\.) .*") + match = list_item_regex.search(line) + return match if match else None def on_tab_key( @@ -44,7 +48,7 @@ def on_tab_key( """Indenting and dedenting list items""" if tab.settings.get("filetype_name", str) == "Markdown": line = event.widget.get("insert linestart", "insert lineend") - list_item_status = _is_list_item(line) + list_item_status = _list_item(line) # shift-tab if shift_pressed and list_item_status: @@ -62,8 +66,26 @@ def on_tab_key( return None +def continue_list(tab: tabs.FileTab, event: tkinter.Event[tkinter.Text]) -> str | None: + """Automatically continue lists + + This happens after the `autoindent` plugin automatically handles indentation + """ + if tab.settings.get("filetype_name", str) == "Markdown": + current_line = event.widget.get("insert - 1l linestart", "insert -1l lineend") + list_item_match = _list_item(current_line) + if list_item_match: + indentation, prefix = list_item_match.groups() + + tab.textwidget.insert("insert", prefix + " ") + tab.update() + + return None + + def on_new_filetab(tab: tabs.FileTab) -> None: utils.bind_tab_key(tab.textwidget, partial(on_tab_key, tab), add=True) + tab.textwidget.bind("<>", partial(continue_list, tab), add=True) def setup() -> None: diff --git a/tests/test_markdown_plugin.py b/tests/test_markdown_plugin.py index 4e5f8d9fb..ed98cc23c 100644 --- a/tests/test_markdown_plugin.py +++ b/tests/test_markdown_plugin.py @@ -9,7 +9,7 @@ from porcupine.plugins import markdown -class IsListItemCase(NamedTuple): +class ListItemCase(NamedTuple): id: str line: str expected: bool @@ -18,31 +18,31 @@ class IsListItemCase(NamedTuple): IS_LIST_ITEM_CASES = [ - IsListItemCase(id="# with no separator", line="# item 1", expected=False), - IsListItemCase(id="# bad separator |", line="#| item 1", expected=False), - IsListItemCase(id="# bad separator /", line="#/ item 1", expected=False), - IsListItemCase(id="# bad separator \\", line="#\\ item 1", expected=False), - IsListItemCase(id="ol bad separator |", line="8| item 1", expected=False), - IsListItemCase(id="ol bad separator /", line="8/ item 1", expected=False), - IsListItemCase(id="ol bad separator \\", line="8\\ item 1", expected=False), - IsListItemCase(id="not a list 1", line="item 1", expected=False), - IsListItemCase(id="not a list 2", line=" item 1", expected=False), - IsListItemCase(id="not a list 3", line=" item 1", expected=False), - IsListItemCase(id="not a list 4", line="& item 1", expected=False), - IsListItemCase(id="not a list 5", line="^ item 1", expected=False), - IsListItemCase(id="duplicate token 1", line="-- item 1", expected=False), - IsListItemCase(id="duplicate token 2", line="--- item 1", expected=False), - IsListItemCase(id="duplicate token 3", line="- - - item 1", expected=True), - IsListItemCase(id="duplicate token 4", line=" - item -- 1 -", expected=True), - IsListItemCase(id="duplicate token 5", line=" -#) item -- 1 -", expected=False), - IsListItemCase(id="duplicate token 6", line=" *-#)1. item -- 1 -", expected=False), + ListItemCase(id="# with no separator", line="# item 1", expected=False), + ListItemCase(id="# bad separator |", line="#| item 1", expected=False), + ListItemCase(id="# bad separator /", line="#/ item 1", expected=False), + ListItemCase(id="# bad separator \\", line="#\\ item 1", expected=False), + ListItemCase(id="ol bad separator |", line="8| item 1", expected=False), + ListItemCase(id="ol bad separator /", line="8/ item 1", expected=False), + ListItemCase(id="ol bad separator \\", line="8\\ item 1", expected=False), + ListItemCase(id="not a list 1", line="item 1", expected=False), + ListItemCase(id="not a list 2", line=" item 1", expected=False), + ListItemCase(id="not a list 3", line=" item 1", expected=False), + ListItemCase(id="not a list 4", line="& item 1", expected=False), + ListItemCase(id="not a list 5", line="^ item 1", expected=False), + ListItemCase(id="duplicate token 1", line="-- item 1", expected=False), + ListItemCase(id="duplicate token 2", line="--- item 1", expected=False), + ListItemCase(id="duplicate token 3", line="- - - item 1", expected=True), + ListItemCase(id="duplicate token 4", line=" - item -- 1 -", expected=True), + ListItemCase(id="duplicate token 5", line=" -#) item -- 1 -", expected=False), + ListItemCase(id="duplicate token 6", line=" *-#)1. item -- 1 -", expected=False), ] # test `#` and 0 to 99 numbered lists # tests ol with `.` and `)` IS_LIST_ITEM_CASES.extend( [ - IsListItemCase(id=f"numbered {i}", line=f"{i}{sep} item 1", expected=True) + ListItemCase(id=f"numbered {i}", line=f"{i}{sep} item 1", expected=True) for i, sep in itertools.product(itertools.chain(range(100), "#"), (".", ")")) ] ) @@ -50,7 +50,7 @@ class IsListItemCase(NamedTuple): # test raw li prefixes with and without space IS_LIST_ITEM_CASES.extend( [ - IsListItemCase( + ListItemCase( id=f"raw prexix {prefix} no space", line=f"{prefix}{' ' if space else ''}", expected=space, @@ -64,7 +64,7 @@ class IsListItemCase(NamedTuple): # test numbered list with whitespace following and preceding IS_LIST_ITEM_CASES.extend( [ - IsListItemCase( + ListItemCase( id=f"numbered {preceding=} {following=} space", line=f"{' ' * preceding}{i}{sep}{' ' * following} item 1", expected=True, @@ -78,7 +78,7 @@ class IsListItemCase(NamedTuple): # test with whitespace following and preceding IS_LIST_ITEM_CASES.extend( [ - IsListItemCase( + ListItemCase( id=f"bullet {preceding=} {following=} space", line=f"{' ' * preceding}{bullet} {' ' * following} item 1", expected=True, @@ -103,7 +103,11 @@ class IsListItemCase(NamedTuple): ) def test_is_list(line: str, expected: bool, raises): with raises: - assert markdown._is_list_item(line) == expected + result = markdown._list_item(line) + if expected: + assert result + if not expected: + assert not result @pytest.mark.parametrize( @@ -185,3 +189,35 @@ def test_non_list(line: str, filetab, tmp_path): assert ( filetab.textwidget.get("1.0", "end - 1 char") == f"{line}\n" ), "should not change, just open autocomplete" + + +@pytest.mark.parametrize( + "li", + [ + "- ", # note the space + "1. ", # note the space + "1. item 1", + "1) item 1", + "#) item 1", + "- item 1", + "* item 1", + "+ item 1", + "+ +++++ weird", + "1) ))))) still weird", + "- [ ] unchecked task", + "- [X] checked task", + ], +) +def test_list_continuation(li: str, filetab, tmp_path): + filetab.textwidget.insert("1.0", li) + filetab.update() + + # switch to Markdown filetype format + filetab.save_as(tmp_path / "asdf.md") + assert filetab.settings.get("filetype_name", object) == "Markdown" + + # new line + filetab.textwidget.event_generate("") + filetab.update() + current_line = filetab.textwidget.get("insert linestart", "insert") + assert markdown._list_item(current_line) From 5ced3d5d0e817147b3f84794a95f491a60f72694 Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Sun, 2 Jul 2023 21:06:47 -0400 Subject: [PATCH 15/27] add update() before all events --- pyproject.toml | 3 +++ pytest.ini | 6 +++--- test.md | 3 +++ tests/test_markdown_plugin.py | 14 +++++++++----- 4 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 test.md diff --git a/pyproject.toml b/pyproject.toml index 5a985ebc4..b022ed29b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,9 @@ line_length = 100 profile = "black" multi_line_output = 3 +[tool.ruff] +ignore = ["E501"] + # Flit configuration ([build-system] and [project]) are used when pip installing with github url. # See commands in README. [build-system] diff --git a/pytest.ini b/pytest.ini index 481e16da1..877bc7e58 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,9 +1,9 @@ [pytest] # why we disable nose plugin: https://github.com/pytest-dev/pytest/issues/10825 -addopts = --doctest-modules --capture=no -p no:nose -testpaths = porcupine/plugins/autoindent.py tests/ +addopts = --doctest-modules -p no:nose --ignore porcupine/plugins/run/windows_run.py +testpaths = tests/ porcupine/ markers = pastebin_test # uncomment this if you dare... i like how pytest hides the shittyness # by default -#log_cli = true +; log_cli = true \ No newline at end of file diff --git a/test.md b/test.md new file mode 100644 index 000000000..6eafa5e43 --- /dev/null +++ b/test.md @@ -0,0 +1,3 @@ +test +foobar + - fo diff --git a/tests/test_markdown_plugin.py b/tests/test_markdown_plugin.py index ed98cc23c..c58f23991 100644 --- a/tests/test_markdown_plugin.py +++ b/tests/test_markdown_plugin.py @@ -131,29 +131,32 @@ def test_filetype_switching(li: str, filetab, tmp_path): assert filetab.settings.get("filetype_name", object) == "Python" filetab.textwidget.insert("1.0", li) - filetab.textwidget.event_generate("") filetab.update() + filetab.textwidget.event_generate("") assert ( filetab.textwidget.get("1.0", "insert") == li ), "should not effect list items unless using markdown filetype" + filetab.update() filetab.textwidget.event_generate("") # close the autocomplete # switch to Markdown filetype format filetab.save_as(tmp_path / "asdf.md") assert filetab.settings.get("filetype_name", object) == "Markdown" - filetab.textwidget.event_generate("") filetab.update() + filetab.textwidget.event_generate("") # no change to text, should open autocomplete menu assert filetab.textwidget.get("1.0", "insert") == li + filetab.update() filetab.textwidget.event_generate("") # close the autocomplete # add a space filetab.textwidget.insert("insert", " ") - filetab.textwidget.event_generate("") filetab.update() + filetab.textwidget.event_generate("") assert filetab.textwidget.get("1.0", "insert") == f" {li} ", "should be indented" + filetab.update() filetab.textwidget.event_generate("") filetab.update() assert filetab.textwidget.get("1.0", "insert") == f"{li} ", "should be back to normal" @@ -183,9 +186,10 @@ def test_non_list(line: str, filetab, tmp_path): assert filetab.settings.get("filetype_name", object) == "Markdown" filetab.textwidget.insert("1.0", line) + filetab.update() filetab.textwidget.event_generate("") - filetab.textwidget.event_generate("") # close the autocomplete filetab.update() + filetab.textwidget.event_generate("") # close the autocomplete assert ( filetab.textwidget.get("1.0", "end - 1 char") == f"{line}\n" ), "should not change, just open autocomplete" @@ -217,7 +221,7 @@ def test_list_continuation(li: str, filetab, tmp_path): assert filetab.settings.get("filetype_name", object) == "Markdown" # new line - filetab.textwidget.event_generate("") filetab.update() + filetab.textwidget.event_generate("") current_line = filetab.textwidget.get("insert linestart", "insert") assert markdown._list_item(current_line) From 503cc2303205f3744c2a091675d95b4238730934 Mon Sep 17 00:00:00 2001 From: Akuli Date: Mon, 3 Jul 2023 22:42:33 +0300 Subject: [PATCH 16/27] Delete test.md --- test.md | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 test.md diff --git a/test.md b/test.md deleted file mode 100644 index 6eafa5e43..000000000 --- a/test.md +++ /dev/null @@ -1,3 +0,0 @@ -test -foobar - - fo From c24298cd0cd86c481371e3522eb3b3e02aa67500 Mon Sep 17 00:00:00 2001 From: Akuli Date: Mon, 3 Jul 2023 22:43:23 +0300 Subject: [PATCH 17/27] Revert unrelated comment style change to pytest.ini --- pytest.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest.ini b/pytest.ini index 8d5fb3445..3263f74ca 100644 --- a/pytest.ini +++ b/pytest.ini @@ -9,4 +9,4 @@ markers = pastebin_test # uncomment this if you dare... i like how pytest hides the shittyness # by default -; log_cli = true +#log_cli = true From 28a4e277e67095aa60de7ab247486fdebfa3f93c Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Mon, 3 Jul 2023 18:58:47 -0400 Subject: [PATCH 18/27] remove print --- porcupine/plugins/markdown.py | 1 - 1 file changed, 1 deletion(-) diff --git a/porcupine/plugins/markdown.py b/porcupine/plugins/markdown.py index a73b3c5fc..5fca22300 100644 --- a/porcupine/plugins/markdown.py +++ b/porcupine/plugins/markdown.py @@ -29,7 +29,6 @@ def _list_item(line: str) -> re.Match[str] | None: - https://pandoc.org/MANUAL.html#lists Technically `#)` is not in either spec, but I won't tell if you won't """ - print(f"{line=}") assert isinstance(line, str) if not line: # empty string From 65c71bd2eddac5203bdbd0bc30107d2ccf24c033 Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Mon, 3 Jul 2023 19:14:04 -0400 Subject: [PATCH 19/27] remove bad test --- tests/test_indent_dedent.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/tests/test_indent_dedent.py b/tests/test_indent_dedent.py index 7eef3d871..733abb7f2 100644 --- a/tests/test_indent_dedent.py +++ b/tests/test_indent_dedent.py @@ -198,28 +198,6 @@ def check(filename, input_commands, output): return check -def test_markdown_autoindent(check_autoindents): - check_autoindents( - "hello.md", - """ -1. Lol and -wat. -- Foo and -bar and -baz. -End of list -""", - """ -1. Lol and - wat. -- Foo and - bar and - baz. -End of list -""", - ) - - def test_shell_autoindent(check_autoindents): check_autoindents( "loll.sh", From 6753a1ea540b888e9dc4da36d74adb12e1472624 Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Mon, 3 Jul 2023 19:16:20 -0400 Subject: [PATCH 20/27] fix crash in case no filetype --- porcupine/plugins/markdown.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/porcupine/plugins/markdown.py b/porcupine/plugins/markdown.py index 5fca22300..0fe987229 100644 --- a/porcupine/plugins/markdown.py +++ b/porcupine/plugins/markdown.py @@ -45,7 +45,7 @@ def on_tab_key( tab: tabs.FileTab, event: tkinter.Event[textutils.MainText], shift_pressed: bool ) -> str | None: """Indenting and dedenting list items""" - if tab.settings.get("filetype_name", str) == "Markdown": + if tab.settings.get("filetype_name", object) == "Markdown": line = event.widget.get("insert linestart", "insert lineend") list_item_status = _list_item(line) @@ -70,7 +70,7 @@ def continue_list(tab: tabs.FileTab, event: tkinter.Event[tkinter.Text]) -> str This happens after the `autoindent` plugin automatically handles indentation """ - if tab.settings.get("filetype_name", str) == "Markdown": + if tab.settings.get("filetype_name", object) == "Markdown": current_line = event.widget.get("insert - 1l linestart", "insert -1l lineend") list_item_match = _list_item(current_line) if list_item_match: From c0ebdc3f36e6189042e449dab641fb93cf9c293b Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Mon, 3 Jul 2023 19:56:33 -0400 Subject: [PATCH 21/27] fix list continuation tests really not sure why this was broken, I'm 100% sure I had this working yesterday.......... --- tests/test_markdown_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_markdown_plugin.py b/tests/test_markdown_plugin.py index c58f23991..44d9b0232 100644 --- a/tests/test_markdown_plugin.py +++ b/tests/test_markdown_plugin.py @@ -223,5 +223,5 @@ def test_list_continuation(li: str, filetab, tmp_path): # new line filetab.update() filetab.textwidget.event_generate("") - current_line = filetab.textwidget.get("insert linestart", "insert") + current_line = filetab.textwidget.get("insert - 1l linestart", "insert - 1l lineend") assert markdown._list_item(current_line) From 997c67dc23395623294ce67e29ce7d4584fbf1b9 Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Mon, 3 Jul 2023 21:46:34 -0400 Subject: [PATCH 22/27] actually fix the list continuation test hopefully --- tests/test_markdown_plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_markdown_plugin.py b/tests/test_markdown_plugin.py index 44d9b0232..fce232691 100644 --- a/tests/test_markdown_plugin.py +++ b/tests/test_markdown_plugin.py @@ -221,7 +221,7 @@ def test_list_continuation(li: str, filetab, tmp_path): assert filetab.settings.get("filetype_name", object) == "Markdown" # new line - filetab.update() filetab.textwidget.event_generate("") - current_line = filetab.textwidget.get("insert - 1l linestart", "insert - 1l lineend") + filetab.update() + current_line = filetab.textwidget.get("insert linestart", "insert lineend") assert markdown._list_item(current_line) From 9aa190a8881d4cc57ec9445606810c6da312cdeb Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Mon, 3 Jul 2023 22:37:04 -0400 Subject: [PATCH 23/27] clear empty list item on return --- porcupine/plugins/markdown.py | 28 +++++++++++++++++++++++----- tests/test_markdown_plugin.py | 34 ++++++++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/porcupine/plugins/markdown.py b/porcupine/plugins/markdown.py index 0fe987229..4e316ec63 100644 --- a/porcupine/plugins/markdown.py +++ b/porcupine/plugins/markdown.py @@ -15,14 +15,15 @@ log = logging.getLogger(__name__) -setup_before = ["tabs2spaces"] +setup_before = ["tabs2spaces", "autoindent"] def _list_item(line: str) -> re.Match[str] | None: """Regex for markdown list item - First group is the whitespace (if any) preceding the item - Second group is the list item prefix (ex `-`, `+`, `6.`, `#.`) + 1st group is the whitespace (if any) preceding the item + 2nd group is the list item prefix (ex `-`, `+`, `6.`, `#.`) + 3rd group is the item text According to: - https://spec.commonmark.org/0.30/#lists @@ -36,7 +37,7 @@ def _list_item(line: str) -> re.Match[str] | None: assert len(line.splitlines()) == 1 - list_item_regex = re.compile(r"(^[\t ]*)(\d{1,9}[.)]|[-+*]|#\)|#\.) .*") + list_item_regex = re.compile(r"(^[\t ]*)(\d{1,9}[.)]|[-+*]|#\)|#\.) (.*)") match = list_item_regex.search(line) return match if match else None @@ -74,7 +75,7 @@ def continue_list(tab: tabs.FileTab, event: tkinter.Event[tkinter.Text]) -> str current_line = event.widget.get("insert - 1l linestart", "insert -1l lineend") list_item_match = _list_item(current_line) if list_item_match: - indentation, prefix = list_item_match.groups() + indentation, prefix, item_text = list_item_match.groups() tab.textwidget.insert("insert", prefix + " ") tab.update() @@ -82,9 +83,26 @@ def continue_list(tab: tabs.FileTab, event: tkinter.Event[tkinter.Text]) -> str return None +def on_enter_press(tab: tabs.FileTab, event: tkinter.Event[tkinter.Text]) -> str | None: + if tab.settings.get("filetype_name", object) == "Markdown": + current_line = event.widget.get("insert linestart", "insert lineend") + list_item_match = _list_item(current_line) + if list_item_match: + indentation, prefix, item_text = list_item_match.groups() + if item_text: + # there is item text, so we are done here + return None + + event.widget.delete("insert linestart", "insert lineend") + return "break" + + return None + + def on_new_filetab(tab: tabs.FileTab) -> None: utils.bind_tab_key(tab.textwidget, partial(on_tab_key, tab), add=True) tab.textwidget.bind("<>", partial(continue_list, tab), add=True) + tab.textwidget.bind("", partial(on_enter_press, tab), add=True) def setup() -> None: diff --git a/tests/test_markdown_plugin.py b/tests/test_markdown_plugin.py index fce232691..b9f1eaeb7 100644 --- a/tests/test_markdown_plugin.py +++ b/tests/test_markdown_plugin.py @@ -198,8 +198,6 @@ def test_non_list(line: str, filetab, tmp_path): @pytest.mark.parametrize( "li", [ - "- ", # note the space - "1. ", # note the space "1. item 1", "1) item 1", "#) item 1", @@ -225,3 +223,35 @@ def test_list_continuation(li: str, filetab, tmp_path): filetab.update() current_line = filetab.textwidget.get("insert linestart", "insert lineend") assert markdown._list_item(current_line) + + +@pytest.mark.parametrize("prefix", ["-", "+", "*", "#.", "#)", "1.", "1)", "88)", "88."]) +def test_return_remove_empty_item(prefix: str, filetab, tmp_path): + """Pressing 'return' on an empty item should remove it""" + filetab.textwidget.insert("1.0", prefix + " item 1") + filetab.update() + + # switch to Markdown filetype format + filetab.save_as(tmp_path / "asdf.md") + assert filetab.settings.get("filetype_name", object) == "Markdown" + + # new line + filetab.textwidget.event_generate("") + filetab.update() + previous_line = filetab.textwidget.get("insert - 1l linestart", "insert - 1l lineend") + current_line = filetab.textwidget.get("insert linestart", "insert lineend") + assert previous_line == f"{prefix} item 1" + assert markdown._list_item(previous_line) + assert current_line == f"{prefix} " + assert markdown._list_item(current_line) + + # new line + filetab.textwidget.event_generate("") + filetab.update() + previous_line = filetab.textwidget.get("insert - 1l linestart", "insert - 1l lineend") + current_line = filetab.textwidget.get("insert linestart", "insert lineend") + assert previous_line == f"{prefix} item 1" + assert markdown._list_item(previous_line) + # current line should now be empty + assert current_line == "" + assert not markdown._list_item(current_line) From 5953040d34c5431b3b6b6c4fc8967c3d1c8260a4 Mon Sep 17 00:00:00 2001 From: Akuli Date: Wed, 5 Jul 2023 11:44:27 +0300 Subject: [PATCH 24/27] Fix pastebin plugin description (#1336) --- porcupine/plugins/pastebin.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/porcupine/plugins/pastebin.py b/porcupine/plugins/pastebin.py index dc3f7e720..7fa6989bf 100644 --- a/porcupine/plugins/pastebin.py +++ b/porcupine/plugins/pastebin.py @@ -4,11 +4,13 @@ 1. Select some code in the editor -2. Select "Pastebin selected text to dpaste.com" (or some other site) +2. Right-click the selected code -3. Wait until you get the link +3. Select "Pastebin selected text to dpaste.com" (or some other site) -4. Send the link to someone else +4. Wait until you get a link + +5. Send the link to someone else """ # docstring above needs to have blank lines to show properly in plugin manager # TODO: make this work with pythonprompt plugin? From f3bc25b9b883d544ed57cc3868ba85bca573b173 Mon Sep 17 00:00:00 2001 From: ThePhilgrim <76887896+ThePhilgrim@users.noreply.github.com> Date: Wed, 5 Jul 2023 23:56:07 +0200 Subject: [PATCH 25/27] Prevent crash when opening file dialog on Mac (#1337) --- .gitignore | 1 + porcupine/plugins/filetypes.py | 29 +++++++++++++++-------------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 553779059..cf4218c6c 100644 --- a/.gitignore +++ b/.gitignore @@ -94,3 +94,4 @@ env*/ # other stuff i have needed to add .pytest_cache playground.* +.vscode/ diff --git a/porcupine/plugins/filetypes.py b/porcupine/plugins/filetypes.py index 097ea410f..8496b74d7 100644 --- a/porcupine/plugins/filetypes.py +++ b/porcupine/plugins/filetypes.py @@ -102,23 +102,24 @@ def load_filetypes() -> None: def set_filedialog_kwargs() -> None: - filedialog_kwargs["filetypes"] = [ - ( - name, - [ - # "*.py" doesn't work on windows, but ".py" works and does the same thing - # See "SPECIFYING EXTENSIONS" in tk_getOpenFile manual page - pattern.split("/")[-1].lstrip("*") - for pattern in filetype["filename_patterns"] - ], - ) - for name, filetype in filetypes.items() - if name != "Plain Text" # can just use "All Files" for this - ] + filedialog_kwargs["filetypes"] = [] + + for name, filetype in filetypes.items(): + # "*.py" doesn't work on windows, but ".py" works and does the same thing + # See "SPECIFYING EXTENSIONS" in tk_getOpenFile manual page + file_patterns = [ + pattern.split("/")[-1].lstrip("*") for pattern in filetype["filename_patterns"] + ] + filedialog_kwargs["filetypes"].append((name, file_patterns)) + + # File types without an extension seem to cause crashes on Mac in certain cases (See issue #1092). if sys.platform != "darwin": - # Causes crashes for some Mac users, but not all. See #1092 filedialog_kwargs["filetypes"].insert(0, ("All Files", ["*"])) + else: + filedialog_kwargs["filetypes"].remove( + ("Makefile", ["Makefile", "makefile", "Makefile.*", "makefile.*"]) + ) def get_filetype_from_matches( From 723c555d66a47e16d6969f15e9f1a80d89a1b9a9 Mon Sep 17 00:00:00 2001 From: ThePhilgrim <76887896+ThePhilgrim@users.noreply.github.com> Date: Thu, 6 Jul 2023 00:09:07 +0200 Subject: [PATCH 26/27] Add Mac & VS Code stuff to .gitignore (#1338) --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index cf4218c6c..53775dbe0 100644 --- a/.gitignore +++ b/.gitignore @@ -90,6 +90,8 @@ env*/ # Rope project settings .ropeproject +# Mac stuff +.DS_Store # other stuff i have needed to add .pytest_cache From af45e4a54945a31e1ebd218e87ecd20ff174911c Mon Sep 17 00:00:00 2001 From: Benjamin Kirkbride Date: Thu, 6 Jul 2023 04:11:58 -0400 Subject: [PATCH 27/27] Testing Improvements (#1327) --- .github/workflows/autofix.yml | 2 +- .github/workflows/check.yml | 9 +++++---- .github/workflows/release-builds.yml | 2 +- tests/test_fullscreen_plugin.py | 7 +++++-- tests/test_jump_to_definition_plugin.py | 8 ++++++++ tests/test_run_plugin.py | 4 ++-- 6 files changed, 22 insertions(+), 10 deletions(-) diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index f06d095ff..45a56cd42 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -19,7 +19,7 @@ jobs: path: ./pr - uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.11" cache: pip - run: pip install wheel - run: pip install -r requirements-dev.txt diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 40f03e58f..6e1bc2f26 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.11" cache: pip - run: pip install wheel - run: pip install -r requirements.txt -r requirements-dev.txt @@ -26,13 +26,14 @@ jobs: time mypy --platform darwin --python-version 3.8 porcupine docs/extensions.py time mypy --platform darwin --python-version 3.9 porcupine docs/extensions.py time mypy --platform darwin --python-version 3.10 porcupine docs/extensions.py + time mypy --platform darwin --python-version 3.11 porcupine docs/extensions.py pytest: timeout-minutes: 10 strategy: matrix: os: ["ubuntu-latest", "windows-latest"] - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.8", "3.9", "3.10", "3.11"] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 @@ -60,7 +61,7 @@ jobs: runs-on: macos-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.8", "3.9", "3.10", "3.11"] env: # TODO: how to install tkdnd on mac? add instructions to README or make mac app that bundles it TCLLIBPATH: ./lib @@ -82,7 +83,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.11" cache: pip # TODO: adding these to requirements-dev.txt breaks pip install - run: pip install flake8==5.0.4 flake8-tkinter==0.5.0 diff --git a/.github/workflows/release-builds.yml b/.github/workflows/release-builds.yml index c55d76781..177f405a2 100644 --- a/.github/workflows/release-builds.yml +++ b/.github/workflows/release-builds.yml @@ -31,7 +31,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.11" cache: pip - run: pip install wheel - run: pip install -r requirements.txt -r requirements-dev.txt diff --git a/tests/test_fullscreen_plugin.py b/tests/test_fullscreen_plugin.py index 56f9fec34..7529d140b 100644 --- a/tests/test_fullscreen_plugin.py +++ b/tests/test_fullscreen_plugin.py @@ -5,8 +5,12 @@ from porcupine import get_main_window from porcupine.menubar import get_menu +headless = os.getenv("GITHUB_ACTIONS") == "true" or "xvfb" in os.getenv("XAUTHORITY", "") + + +pytestmark = pytest.mark.skipif(headless, reason="Does not work in headless environments") + -@pytest.mark.skipif(os.getenv("GITHUB_ACTIONS") == "true", reason="fails CI on all platforms") def test_basic(wait_until): assert not get_main_window().attributes("-fullscreen") @@ -18,7 +22,6 @@ def test_basic(wait_until): # Window managers can toggle full-screen-ness without going through our menubar -@pytest.mark.skipif(os.getenv("GITHUB_ACTIONS") == "true", reason="fails CI on all platforms") def test_toggled_without_menu_bar(wait_until): get_main_window().attributes("-fullscreen", 1) wait_until(lambda: bool(get_main_window().attributes("-fullscreen"))) diff --git a/tests/test_jump_to_definition_plugin.py b/tests/test_jump_to_definition_plugin.py index b0c6c8692..19cd39491 100644 --- a/tests/test_jump_to_definition_plugin.py +++ b/tests/test_jump_to_definition_plugin.py @@ -1,11 +1,19 @@ # TODO: create much more tests for langserver +import sys import time +import pytest from sansio_lsp_client import ClientState from porcupine import get_main_window from porcupine.plugins.langserver import langservers +pytestmark = pytest.mark.xfail( + sys.version_info >= (3, 11), + strict=True, + reason="https://github.com/Akuli/porcupine/issues/1300", +) + def langserver_started(filetab): return lambda: any( diff --git a/tests/test_run_plugin.py b/tests/test_run_plugin.py index fb6809518..dcbc03221 100644 --- a/tests/test_run_plugin.py +++ b/tests/test_run_plugin.py @@ -306,7 +306,7 @@ def test_python_unbuffered(tmp_path, wait_until): no_terminal.run_command(f"{utils.quote(sys.executable)} sleeper.py", tmp_path) wait_until(lambda: "This should show up immediately" in get_output()) end = time.monotonic() - assert end - start < 8 + assert end - start < 9 def test_not_line_buffered(tmp_path, wait_until): @@ -321,7 +321,7 @@ def test_not_line_buffered(tmp_path, wait_until): no_terminal.run_command(f"{utils.quote(sys.executable)} sleeper.py", tmp_path) wait_until(lambda: "This should show up immediately" in get_output()) end = time.monotonic() - assert end - start < 8 + assert end - start < 9 def test_crlf_on_any_platform(tmp_path, wait_until):