Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scroll position sync support #43

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion markdown_preview/main_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .utils import get_backends_dict, init_gettext, recognize_format
from .webview_manager import MdWebViewManager
from .constants import MD_PREVIEW_KEY_BASE, MARKDOWN_SPLITTERS, BASE_TEMP_NAME
from .source_lines import SourceLinesExtension

AVAILABLE_BACKENDS = get_backends_dict()
if AVAILABLE_BACKENDS['p3md']:
Expand Down Expand Up @@ -278,6 +279,9 @@ def _on_reload_unsafe(self):
return
start, end = doc.get_bounds()
unsaved_text = doc.get_text(start, end, True)
cursor_position = doc.get_property("cursor-position")
text_iter = doc.get_iter_at_offset(cursor_position)
self._webview_manager.set_cursor_position (text_iter.get_line()+1, text_iter.get_line_offset()+1)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why "+1"? does it not understand zeros? then isn't it problematic that you set cursor_row and column as 0 in the init?

unsaved_text = unsaved_text.encode('utf-8').decode()
if self._file_format == 'html':
html_content = self.get_html_from_html(unsaved_text)
Expand Down Expand Up @@ -343,9 +347,10 @@ def get_html_from_p3md(self, unsaved_text):
'rel="stylesheet" href="' + self._stylesheet + '" /></head><body>'
post_string = '</body></html>'

extensions = [SourceLinesExtension()]+self._p3md_extensions
html_string = markdown.markdown(
unsaved_text,
extensions=self._p3md_extensions
extensions=extensions
)
html_content = pre_string + html_string + post_string
return html_content
Expand Down
2 changes: 1 addition & 1 deletion markdown_preview/prefs/prefs_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ def set_command_for_format(self, output_format):
return

command = 'pandoc $INPUT_FILE %s'
options = '--metadata pagetitle=Preview'
options = '-f commonmark+sourcepos --metadata pagetitle=Preview'
accept_css = True
# TODO........

Expand Down
60 changes: 60 additions & 0 deletions markdown_preview/source_lines.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# python3 -m markdown -x source_lines:SourceLinesExtension example.md

from markdown.extensions import Extension
from markdown.blockparser import BlockParser
from markdown.blockprocessors import BlockProcessor
import re
import types
import xml.etree.ElementTree as etree

def set_source_position(block, line):
class BlockWithSourcePosition(type(block)):
def set_source_position(self, line):
self.source_line = line
return self
return BlockWithSourcePosition(block).set_source_position(line)

def parseBlocks(self, parent, _blocks):
if not BlockParser.first_run:
self._parseBlocks(parent, _blocks)
return
BlockParser.first_run = False
source_line = 1
blocks = []
for b in _blocks:
new_lines = re.search('[^\n]|$', b, re.MULTILINE).start()
blocks.append(set_source_position(b, source_line+new_lines))
source_line += b.count('\n')+2
self._parseBlocks(parent, blocks)

def block_processor_run(self, parent, blocks):
if len(parent)>0 and not parent[-1].get("source-line"):
parent[-1].set("source-line", str(BlockParser.source_line))
setattr(BlockParser, "source_line", None)
try:
setattr(BlockParser, "source_line", blocks[0].source_line)
except Exception:
pass
result = self._run(parent, blocks)
if len(parent)>0 and not parent[-1].get("source-line") and BlockParser.source_line:
parent[-1].set("source-line", str(BlockParser.source_line))
setattr(BlockParser, "source_line", None)
return result

_parseBlocks = BlockParser.parseBlocks
BlockProcessor_run = BlockProcessor.run

class SourceLinesExtension(Extension):
def extendMarkdown(self, md):
BlockParser._parseBlocks = _parseBlocks
BlockParser.parseBlocks = parseBlocks
BlockParser.first_run = True
md.parser._parseBlocks = types.MethodType(BlockParser._parseBlocks, md.parser)
md.parser.parseBlocks = types.MethodType(parseBlocks, md.parser)
setattr(BlockParser, "source_line", None)

BlockProcessor._run = BlockProcessor_run
BlockProcessor.run = block_processor_run
for b in md.parser.blockprocessors:
b._run = b.run
b.run = types.MethodType(block_processor_run, b)
76 changes: 74 additions & 2 deletions markdown_preview/webview_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,79 @@

_ = init_gettext()

SCRIPT_SCROLL_TO_CURSOR = """
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the script could be in a separate file, and loaded in the init. This way, it could be a js file with syntax coloration

window.document.body.scrollTop = %d;
var cursorRow = %d;
var cursorColumn = %d;
function setCursorPositionPandoc () {
var dataPosElements = window.document.querySelectorAll("[data-pos]");
var dataPosRegExp = new RegExp(".*@(?<start_row>[0-9]*):(?<start_column>[0-9]*)-(?<end_row>[0-9]*):(?<end_column>[0-9]*)");
var values = "";
for (let e of dataPosElements) {
var dataPos = e.getAttribute("data-pos");
var matches = dataPosRegExp.exec(dataPos, "g");

if (!matches) {
continue;
}

values += cursorRow+" "+cursorColumn+"\\n";
values += startRow+" "+startColumn+" "+endRow+" "+endColumn+"\\n";

var startRow = parseInt(matches.groups.start_row);
var startColumn = parseInt(matches.groups.start_column);
var endRow = parseInt(matches.groups.end_row);
var endColumn = parseInt(matches.groups.end_column);
if (startRow<=cursorRow && cursorRow<=endRow &&
(startRow!==cursorRow || startColumn<=cursorColumn) &&
(endRow!==cursorRow || cursorColumn<=endColumn)) {
e.scrollIntoViewIfNeeded();
return true;
}
}
return false;
}

function setCursorPositionPythonMarkdown() {
var sourceLineElements = window.document.querySelectorAll("[source-line]");
var highestLowerBoundLine = -1;
var highestLowerBoundElement = null;
for (let e of sourceLineElements) {
let sourceLine = parseInt(e.getAttribute("source-line"));
if (sourceLine<=cursorRow && highestLowerBoundLine<sourceLine) {
highestLowerBoundLine = sourceLine;
highestLowerBoundElement = e;
}
}
if (highestLowerBoundElement) {
highestLowerBoundElement.scrollIntoViewIfNeeded();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why "if needed"? isn't the standard just "scrollIntoView"?

return true;
}
return false;
}

let cursorUpdateFunctions = [setCursorPositionPandoc, setCursorPositionPythonMarkdown];
for (let f of cursorUpdateFunctions) {
try {
if (f ()) {
break;
}
}
catch (e) {
window.alert(e);
}
}
"""

################################################################################

class MdWebViewManager():

def __init__(self):
self._scroll_level = 0
self.cursor_row = 0
self.cursor_column = 0

# TODO remember the scroll level in the Gedit.View objects

self._webview = WebKit2.WebView()
Expand All @@ -28,6 +95,10 @@ def __init__(self):
self._handlers.append(id3)
self._handlers.append(id4)

def set_cursor_position (self, row, column):
self.cursor_row = row
self.cursor_column = column

def add_find_manager(self, ui_builder):
options = WebKit2.FindOptions.CASE_INSENSITIVE
MdFindManager(ui_builder, self._webview.get_find_controller(), options)
Expand Down Expand Up @@ -79,8 +150,9 @@ def on_remember_scroll(self, *args):
return

def on_restore_scroll(self, *args):
js = 'window.document.body.scrollTop = ' + str(self._scroll_level) + '; undefined;'
self._webview.run_javascript(js, None, None, None)
self._webview.run_javascript(SCRIPT_SCROLL_TO_CURSOR %(self._scroll_level,
self.cursor_row, self.cursor_column),
None, None, None)
return

def javascript_finished(self, webview, result, user_data):
Expand Down
2 changes: 1 addition & 1 deletion org.gnome.gedit.plugins.markdown_preview.gschema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
<description></description>
</key>
<key type="as" name="pandoc-command">
<default>['pandoc', '-s', '$INPUT_FILE', '--metadata', 'pagetitle=Preview']</default>
<default>['pandoc', '-f', 'commonmark+sourcepos', '-s', '$INPUT_FILE', '--metadata', 'pagetitle=Preview']</default>
<summary>Pandoc rendering command line</summary>
<description>
The command line used for pandoc rendering. It has to return HTML code
Expand Down