Community
    • Login

    Feature Request / Question: Soft Wrap at Vertical Edge (Column 80) regardless of window size

    Scheduled Pinned Locked Moved Help wanted · · · – – – · · ·
    16 Posts 7 Posters 518 Views
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • Thorsten HeuerT
      Thorsten Heuer
      last edited by

      Hi everyone,

      I am using Notepad++ v8.9 (64-bit) and I am looking for a way to achieve a specific wrapping behavior.

      My Goal: I want the text to “soft wrap” (visual wrap only, no CR/LF added to the file) exactly at the 80th column (at the last whitespace before 80).

      The Problem: Currently, “Word Wrap” always wraps at the right window border. If my monitor is wide and the window is maximized, the text stretches across the whole screen. I have already set the “Vertical Edge” to 80, but this is just a visual line and doesn’t force the text to wrap there.

      My Questions:

      Is there any native setting I missed to force the soft wrap at the Vertical Edge column instead of the window border?

      If not, is there a Plugin (compatible with v8.9) or a Python Script that can simulate this behavior?

      I want to keep the file “clean” (no hard breaks) for further processing, but I need the 80-character readability for my creative writing (lyrics).

      Thanks in advance for your help!

      h-jangraH 1 Reply Last reply Reply Quote 1
      • h-jangraH
        h-jangra @Thorsten Heuer
        last edited by

        @Thorsten-Heuer Though I didn’t used this much but I have added this in NppVim plugin you :set tw=60 and :wrap or gq motion. I am not sure how to set text width in notepad++.

        fml2F Thorsten HeuerT 3 Replies Last reply Reply Quote 0
        • fml2F
          fml2 @h-jangra
          last edited by

          @h-jangra said in Feature Request / Question: Soft Wrap at Vertical Edge (Column 80) regardless of window size:

          I have added this in NppVim plugin [. . .]I am not sure how to set text width in notepad++

          That’s interesting! I’d think that a NP++ plugin can only do things that are also possible in NP++ itself since the underlying engine is the same.

          CoisesC 1 Reply Last reply Reply Quote 0
          • Thorsten HeuerT
            Thorsten Heuer @h-jangra
            last edited by

            @h-jangra Bild1.png I apologize, but I can’t find a way to set “:set tw=60 and :wrap or gq” in “NppVim”. I’m not a very experienced user; perhaps I’m missing some basic knowledge.

            h-jangraH 2 Replies Last reply Reply Quote 0
            • h-jangraH
              h-jangra @Thorsten Heuer
              last edited by

              @Thorsten-Heuer You can set the text width in command mode by typing :set tw=80. This defines an 80-column wrap limit.

              After that, use V to select the line (visual line mode) and press gq to reflow it according to that column width.

              Currently, this setting isn’t persistent between sessions. If you’d like it to be configurable (similar to Vim), feel free to open an issue, I can look into adding support for it.

              1 Reply Last reply Reply Quote 0
              • h-jangraH
                h-jangra @Thorsten Heuer
                last edited by

                @Thorsten-Heuer
                You can set vertical edge though but then wrap will not work according to that.

                • Settings → Preferences → Margins/Border/Edge
                • Enable Vertical Edge
                • Set it to 80

                86b1a92a-f3ff-4215-b340-a558e78e6319-image.png

                1 Reply Last reply Reply Quote 0
                • fml2F
                  fml2 @h-jangra
                  last edited by

                  @h-jangra I’ve tried it out:

                  1. Installed the NppVim plugin
                  2. entered the commands set tw=30 and wrap.

                  After the first command, a thin vertical line is displayed at the position 30; the second command activates text wrapping. But, as in the “usual” NP++, the wrapping is made at the windows right edge, not at the width set with tw.

                  It works exactly the same way it works in NP++ without NppVim. Which is not surprising because everything is backed by NP++ and Scintilla in the end.

                  It’s very surprising that Scintilla does not have an option to soft wrap the text at a specified width (not window edge).

                  CoisesC h-jangraH 2 Replies Last reply Reply Quote 0
                  • CoisesC
                    Coises @fml2
                    last edited by

                    @fml2 said in Feature Request / Question: Soft Wrap at Vertical Edge (Column 80) regardless of window size:

                    It’s very surprising that Scintilla does not have an option to soft wrap the text at a specified width (not window edge).

                    I must be missing something… it’s very surprising to me that if this is important to you, you wouldn’t just resize the window so the edge is where you want the file to wrap.

                    Two alternatives (though I admit I don’t see why either would be better than just resizing the window):

                    • Open something in a docking panel, like View | Document Map, and drag the divider so that it lines up with the place you want to wrap.

                    • Open a new tab, move it to the second view, and drag the divider to the place you want to wrap.

                    M Andre Z EckenrodeM 1 Reply Last reply Reply Quote 0
                    • M Andre Z EckenrodeM
                      M Andre Z Eckenrode @Coises
                      last edited by

                      @Coises

                      Not that soft wrapping of text at a specified column other than window width is something I do, but I can think of a few reasons why one may wish to avoid resizing the window, particularly so that it’s only 80 columns wide, to accomplish the desired effect: So that the menus and tool bar aren’t adversely affected.

                      If I wanted to achieve custom soft wrapping myself, I’d prefer NOT changing my window size specifically because I prefer having NPP open in the same custom location and size every time I run it, and I’d have to reverse my window size changes before closing NPP in order to maintain that.

                      One could also increase the zoom level until the desired wrapping occurs, though that, too, would have the potentially negative side effect of reducing the amount of text visible at a time.

                      1 Reply Last reply Reply Quote 0
                      • CoisesC
                        Coises @fml2
                        last edited by

                        @fml2 said in Feature Request / Question: Soft Wrap at Vertical Edge (Column 80) regardless of window size:

                        That’s interesting! I’d think that a NP++ plugin can only do things that are also possible in NP++ itself since the underlying engine is the same.

                        There is a way to do it in Scintilla:
                        SCI_SETMARGINLEFT and SCI_SETMARGINRIGHT set margins, in pixels.

                        A plugin, or a PythonScript, could determine the displayable text width of the Scintilla control (I think that would require getting the client width of the Scintilla control with GetClientRect, then using SCI_GETMARGINS to find the number of active margins and iterating through them with SCI_GETMARGINWIDTHN to subtract the margins; there might be an easier way that hasn’t occurred to me) and the width of a string of characters of the desired length (SCI_TEXTWIDTH with style 0), subtract, divide by two and set that margin on each side.

                        fml2F 1 Reply Last reply Reply Quote 3
                        • fml2F
                          fml2 @Coises
                          last edited by

                          @Coises said in Feature Request / Question: Soft Wrap at Vertical Edge (Column 80) regardless of window size:

                          There is a way to do it in Scintilla:
                          SCI_SETMARGINLEFT and SCI_SETMARGINRIGHT set margins, in pixels.

                          Not that I’d use it much, but there were a couple of times I wished the feature was there. Now you have pointed out what the underlying engine can do for this (it works, I’ve checked!).

                          For the real life, it’s of no use though. Who would want to set the width in pixels? To be usable by an end user, it needs a script that would calculate pixel width for the current font and also handle widows resize.

                          These are just theoretical thoughts since you also advised a practicl way to set the desired width (with side panels).

                          Thank you!

                          PeterJonesP 1 Reply Last reply Reply Quote 1
                          • h-jangraH
                            h-jangra @fml2
                            last edited by

                            @fml2 Yes, currently gq in visual line mode works with custom width. I haven’t thought about that and didn’t update wrap command.

                            1 Reply Last reply Reply Quote 0
                            • PeterJonesP
                              PeterJones @fml2
                              last edited by PeterJones

                              @fml2 said in Feature Request / Question: Soft Wrap at Vertical Edge (Column 80) regardless of window size:

                              To be usable by an end user, it needs a script that would calculate pixel width for the current font and also handle widows resize.

                              Which is why @Coises suggested that the plugin or PythonScript do the calculation, including getting the active window (or really, editor sub-window) size, the margins, and calculating character width based on the font size.

                              The one thing he didn’t suggest is hooking this function to a specific notification (for “handle windows resize”), but that was rather implied by the solution. In case you’re not sure, I believe that handling SCN_UPDATEUI is the right notification, because there isn’t one specific to resize. In PythonScript, that hook would be set using editor.callback(functionNameHere, [SCINTILLANOTIFICATION.UPDATEUI]) – so functionNameHere() would be the function that does the calculations and then sets the marginleft/marginright. (personally, I’d recommend just changing the margin-right based on the full width, rather than dividing it by 2 and splitting between left-and-right)

                              PeterJonesP 1 Reply Last reply Reply Quote 2
                              • PeterJonesP
                                PeterJones @PeterJones
                                last edited by

                                My rough implementation of @Coises suggestion is as follows

                                # encoding=utf-8
                                """in response to https://community.notepad-plus-plus.org/topic/27351/
                                
                                Trying to implement @Coises idea for setting the wrap to exactly 80
                                """
                                from Npp import *
                                import ctypes
                                from ctypes import wintypes
                                
                                # Define the RECT structure to match Win32 API
                                class RECT(ctypes.Structure):
                                    _fields_ = [
                                        ("left", wintypes.LONG),
                                        ("top", wintypes.LONG),
                                        ("right", wintypes.LONG),
                                        ("bottom", wintypes.LONG)
                                    ]
                                
                                    def width(self):
                                        return self.right - self.left
                                
                                    def height(self):
                                        return self.bottom - self.top
                                
                                def pysc_setWrap80(ed=editor):
                                    #console.write("ed={}\n".format(ed))
                                
                                    WRAPCHARS = 80
                                
                                    # Setup the Win32 function prototype
                                    user32 = ctypes.windll.user32
                                    user32.GetClientRect.argtypes = [wintypes.HWND, ctypes.POINTER(RECT)]
                                    user32.GetClientRect.restype = wintypes.BOOL
                                
                                    def get_window_size(hwnd):
                                        # 2. Instantiate the RECT structure
                                        rect = RECT()
                                
                                        # 3. Call GetClientRect passing the rect by reference
                                        if user32.GetClientRect(hwnd, ctypes.byref(rect)):
                                            # 4. Parse the results
                                            # Client coordinates: top-left is always (0,0)
                                            return rect
                                        else:
                                            raise Exception("GetClientRect failed")
                                
                                    sz = get_window_size(ed.hwnd)
                                    #console.write("{} => {}\n".format(ed.hwnd, {"width": sz.width(), "height": sz.height()}))
                                
                                    usableWidth = sz.width()
                                    for m in range(0, 1+ed.getMargins()):
                                        w = ed.getMarginWidthN(m)
                                        usableWidth -= w
                                        #console.write("m#{}: {} => usableWidth: {}\n".format(m, w, usableWidth))
                                
                                    widthWrappedChars = ed.textWidth(0,"_"*WRAPCHARS)+1 # one extra pixel to be able to show the VerticalEdge indicator line
                                    wantMargin = usableWidth - widthWrappedChars
                                    if wantMargin < 1:
                                        wantMargin = 0
                                    #console.write("{}\n".format({"windowWidth": sz.width(), "usableWidth": usableWidth, "pixelsFor80Char": widthWrappedChars, "wantMargin": wantMargin}))
                                    ed.setMarginRight(wantMargin)
                                    ed.setMarginLeft(0)
                                
                                def pysc_setWrap80e1(args=None):
                                    pysc_setWrap80(editor1)
                                
                                def pysc_setWrap80e2(args=None):
                                    pysc_setWrap80(editor2)
                                
                                def pysc_setWrap80eX(args=None):
                                    pysc_setWrap80(editor)
                                
                                editor.callback(pysc_setWrap80eX, [SCINTILLANOTIFICATION.PAINTED])
                                console.write("SetWrap80 registered callback\n")
                                

                                (this script tested in PythonScript 3)

                                The FAQ (https://community.notepad-plus-plus.org/topic/23039/faq-how-to-install-and-run-a-script-in-pythonscript) explains how to run a script, or how to make it run automatically at startup

                                The script registers the PAINTED notification – I found that UPDATEUI doesn’t happen everytime you change the window width, whereas PAINTED does… but it means that the callback is running a lot. I wish I knew of a notification that was better suited to just-on-resize, but I don’t. Maybe one of the other PythonScript experts can chime in with a better notification to use (or other suggestions for improvements). I just figured I’d do a proof of concept.

                                If you aren’t changing window size all that often, then it would be better to make a copy of that script that just re-adjusts the 80-character margin on demand (ie, when you run the script) by not using the editor.callback(...) line, and instead just calling pysc_setWrap80(editor) at the end.

                                Alan KilbornA 1 Reply Last reply Reply Quote 0
                                • PeterJonesP PeterJones referenced this topic on
                                • Alan KilbornA
                                  Alan Kilborn @PeterJones
                                  last edited by

                                  @PeterJones said:

                                  I wish I knew of a notification that was better suited to just-on-resize

                                  Hook the message loop and look for WM_SIZE messages?

                                  PeterJonesP 1 Reply Last reply Reply Quote 0
                                  • PeterJonesP
                                    PeterJones @Alan Kilborn
                                    last edited by

                                    @Alan-Kilborn ,

                                    Okay, since I didn’t know how to hook the message loop I found this old post by you (I remembered you had done it for the alt+scrollwheel, thankfully), and I was able to use that as a basis for a two-file solution to @fml2’s request

                                    file 1: MsgHooker.py

                                    # -*- coding: utf-8 -*-
                                    # original author: @Alan-Kilborn
                                    # reference: https://community.notepad-plus-plus.org/post/100127
                                    # modified by: @PeterJones
                                    #   - updated to add the .unhook() and .__del__() methods
                                    
                                    import platform
                                    from ctypes import (WinDLL, WINFUNCTYPE)
                                    from ctypes.wintypes import (HWND, INT, LPARAM, UINT, WPARAM)
                                    
                                    user32 = WinDLL('user32')
                                    
                                    GWL_WNDPROC = -4  # used to set a new address for the window procedure
                                    
                                    LRESULT = LPARAM
                                    
                                    WndProcType = WINFUNCTYPE(
                                        LRESULT,  # return type
                                        HWND, UINT, WPARAM, LPARAM  # function arguments
                                        )
                                    
                                    running_32bit = platform.architecture()[0] == '32bit'
                                    SetWindowLong = user32.SetWindowLongW if running_32bit else user32.SetWindowLongPtrW
                                    SetWindowLong.restype = WndProcType
                                    SetWindowLong.argtypes = [ HWND, INT, WndProcType ]
                                    
                                    class MH(object):
                                    
                                        def __init__(self,
                                                hwnd_to_hook_list,
                                                hook_function,  # supplied hook_function must have args:  hwnd, msg, wparam, lparam
                                                                #  and must return True/False (False means the function handled the msg)
                                                msgs_to_hook_list=None,  # None means ALL msgs
                                                ):
                                            self.users_hook_fn = hook_function
                                            self.msg_list = msgs_to_hook_list if msgs_to_hook_list is not None else []
                                            self.new_wnd_proc_hook_for_SetWindowLong = WndProcType(self._new_wnd_proc_hook)  # the result of this call must be a self.xxx variable!
                                            self.orig_wnd_proc_by_hwnd_dict = {}
                                            for h in hwnd_to_hook_list:
                                                self.orig_wnd_proc_by_hwnd_dict[h] = SetWindowLong(h, GWL_WNDPROC, self.new_wnd_proc_hook_for_SetWindowLong)
                                                v = self.orig_wnd_proc_by_hwnd_dict[h]
                                                #print(f"add {h:08x} => {v}")
                                    
                                        def __del__(self):
                                            self.unhook()
                                    
                                        def unhook(self):
                                            #print(f"unhook: self:{self} => <{self.orig_wnd_proc_by_hwnd_dict}>");
                                            mykeys = []
                                            for h in self.orig_wnd_proc_by_hwnd_dict.keys():
                                                orig = self.orig_wnd_proc_by_hwnd_dict[h]
                                                print(f"\tdel {h:08x} => {orig}")
                                                SetWindowLong(h, GWL_WNDPROC, orig)
                                                mykeys.append(h)
                                            for h in mykeys:
                                                del self.orig_wnd_proc_by_hwnd_dict[h]
                                    
                                        def _new_wnd_proc_hook(self, hwnd, msg, wParam, lParam):
                                            retval = True  # assume that this message will go unhandled (by us)
                                            need_to_call_orig_proc = True
                                            if len(self.msg_list) == 0 or msg in self.msg_list:
                                                retval = self.users_hook_fn(hwnd, msg, wParam, lParam)
                                                if not retval: need_to_call_orig_proc = False
                                            if need_to_call_orig_proc:
                                                retval = self.orig_wnd_proc_by_hwnd_dict[hwnd](hwnd, msg, wParam, lParam)
                                    
                                            return retval
                                    

                                    file 2: the main script:

                                    # encoding=utf-8
                                    """in response to https://community.notepad-plus-plus.org/topic/27351/
                                    
                                    Trying to implement @Coises idea for setting the wrap to exactly 80
                                    """
                                    from Npp import *
                                    import ctypes
                                    from ctypes import wintypes
                                    from MsgHooker import MH as MsgHook
                                    WM_SIZE = 0x0005
                                    
                                    # Define the RECT structure to match Win32 API
                                    class RECT(ctypes.Structure):
                                        _fields_ = [
                                            ("left", wintypes.LONG),
                                            ("top", wintypes.LONG),
                                            ("right", wintypes.LONG),
                                            ("bottom", wintypes.LONG)
                                        ]
                                    
                                        def width(self):
                                            return self.right - self.left
                                    
                                        def height(self):
                                            return self.bottom - self.top
                                    
                                    def pysc_setWrap80(ed=editor):
                                        #console.write("ed={}\n".format(ed))
                                    
                                        WRAPCHARS = 80
                                    
                                        # Setup the Win32 function prototype
                                        user32 = ctypes.windll.user32
                                        user32.GetClientRect.argtypes = [wintypes.HWND, ctypes.POINTER(RECT)]
                                        user32.GetClientRect.restype = wintypes.BOOL
                                    
                                        def get_window_size(hwnd):
                                            # 2. Instantiate the RECT structure
                                            rect = RECT()
                                    
                                            # 3. Call GetClientRect passing the rect by reference
                                            if user32.GetClientRect(hwnd, ctypes.byref(rect)):
                                                # 4. Parse the results
                                                # Client coordinates: top-left is always (0,0)
                                                return rect
                                            else:
                                                raise Exception("GetClientRect failed")
                                    
                                        sz = get_window_size(ed.hwnd)
                                        #console.write("{} => {}\n".format(ed.hwnd, {"width": sz.width(), "height": sz.height()}))
                                    
                                        usableWidth = sz.width()
                                        for m in range(0, 1+ed.getMargins()):
                                            w = ed.getMarginWidthN(m)
                                            usableWidth -= w
                                            #console.write("m#{}: {} => usableWidth: {}\n".format(m, w, usableWidth))
                                    
                                        widthWrappedChars = ed.textWidth(0,"_"*WRAPCHARS)+1 # one extra pixel to be able to show the VerticalEdge indicator line
                                        wantMargin = usableWidth - widthWrappedChars
                                        if wantMargin < 1:
                                            wantMargin = 0
                                        #console.write("{}\n".format({"windowWidth": sz.width(), "usableWidth": usableWidth, "pixelsFor80Char": widthWrappedChars, "wantMargin": wantMargin}))
                                        ed.setMarginRight(wantMargin)
                                        ed.setMarginLeft(0)
                                    
                                    def HIWORD(value): return (value >> 16) & 0xFFFF
                                    def LOWORD(value): return value & 0xFFFF
                                    
                                    def pysc_size_callback( hwnd, msg, wParam, lParam):
                                        #console.write(f"cb(h:{hwnd}, m:{msg}, w:{wParam}, l:{lParam}) => {LOWORD(lParam)} x {HIWORD(lParam)}\n")
                                        if hwnd == editor1.hwnd:
                                            pysc_setWrap80(editor1)
                                        elif hwnd == editor2.hwnd:
                                            pysc_setWrap80(editor2)
                                        return True
                                    
                                    pysc_setWrap80(editor1)
                                    pysc_setWrap80(editor2)
                                    pysc_size_hook = MsgHook([ editor1.hwnd, editor2.hwnd ], pysc_size_callback, [WM_SIZE])
                                    console.write("SetWrap80 registered WM_SIZE callback\n")
                                    
                                    def pysc_unsetWrap80(args=None):
                                        """
                                        To stop
                                        pysc_unsetWrap80()
                                        """
                                        editor1.setMarginRight(0)
                                        editor2.setMarginRight(0)
                                        global pysc_size_hook
                                        if pysc_size_hook:
                                            pysc_size_hook.unhook()
                                        del pysc_size_hook
                                    
                                    # use the following in the console (no #) to stop it from always wrapping at 80
                                    #
                                    # pysc_unsetWrap80()
                                    

                                    You run the main script (or call it from startup.py) to get it to start watching for WM_SIZE, and when it gets that, it does the calcs needed to keep the wrap margin at 80 characters (+1 pixel so that the Vertical Edge line would be visible if you’ve got it on)

                                    Both scripts should go in the main %AppData%\Notepad++\plugins\Config\PythonScript\scripts directory (or equivalent for non-AppData setups, or you need to have added their parent directory to sys.path)

                                    1 Reply Last reply Reply Quote 2
                                    • PeterJonesP PeterJones referenced this topic
                                    • First post
                                      Last post
                                    The Community of users of the Notepad++ text editor.
                                    Powered by NodeBB | Contributors