Tutorial 35: RichEdit Control: Syntax Hilighting

Before reading this tutorial, let me warn you that it's a complicated subject: not suited for a beginner. This is the last in the richedit control tutorials.
 

Theory

Syntax hilighting is a subject of hot debate for those writing text editors. The best method (in my opinion) is to code a custom edit control and this is the approach taken by lots of commercial softwares. However, for those of us who don't have time for coding such control, the next best thing is to adapt the existing control to make it suit our need.

Let us take a look at what RichEdit control provides to help us in implementing syntax hilighting. I should state at this moment that the following method is not the "correct" path: I just want to show you the pitfall that many fall for. RichEdit control provides EM_SETCHARFORMAT message that you can use to change the color of the text. At first glance, this message seems to be the perfect solution (I know because I was one of the victim). However, a closer examination will show you several things that are undesirable:

With the above discussion, you can see that using EM_SETCHARFORMAT is a wrong choice. I'll show you the "relatively correct" choice.

The method I currently use is "syntax hilighting just-in-time". I'll hilight only the visible portion of text. Thus the speed of the hilighting will not be related to the size of the file at all. No matter how large the file, only a small portion of it is visible at one time.

How to do that? The answer is simple:

  1. subclass the richedit control and handle WM_PAINT message within your own window procedure
  2. When it receives WM_PAINT message, it calls the original window procedure of the richedit control to let it update the screen as usual.
  3. After that, we overwrite the words to be hilighted with different color
Of course, the road is not that easy: there are still a couple of minor things to fix but the above method works quite nicely. The display speed is very satisfactory.

Now let's concentrate on the detail. The subclassing process is simple and doesn't require much attention. The really complicated part is when we have to find a fast way of searching for the words to be hilighted. This is further complicated by the need not to hilight any word within a comment block.

The method I use may not be the best but it works ok. I'm sure you can find a faster way. Anyway, here it is:

                        [WORDINFO:
                                WordLen: D§ ?            ; the length of the word: used as a quick comparison    
                                pszWord: D§ ?            ; pointer to the word 
                                pColor: D§ ?             ; point to the dword that contains the color used to hilite the word 
                                NextLink: D§ ?]          ; point to the next WORDINFO structure 
As you can see, I use the length of the word as the second quick comparison. If the first character of the word matches, we next compare its length to the available words. Each dword in ASMSyntaxArray contains a pointer to the head of the associated WORDINFO array. For example, the dword that represents the character "i" will contain the pointer to the linked list of the words that begin with "i". pColor member points to the dword that contains the color value used to hilight the word. pszWord points to the word to be hilighted, in lowercase.
The word list is stored in a file named "wordfile.txt" and I access it with GetPrivateProfileString APIs. I provide as many as 10 different syntax coloring, starting from C1 to C10. The color array is named ASMColorArray. pColor member of each WORDINFO structure points to one of the dwords in ASMColorArray. Thus it is easy to change the syntax coloring on the fly: you just change the dword in ASMColorArray and all words using that color will use the new color immediately.

Example

____________________________________________________________________________________________
[WORDINFO.WordLen 0     ; the length of the word: used as a quick comparison
 WORDINFO.pszWord 4     ; pointer to the word
 WORDINFO.pColor 8      ; point to the dword that contains the color used to hilite the word
 WORDINFO.NextLink 12    ; point to the next WORDINFO structure
 WordInfoLen 16]
 
[IDD_OPTIONDLG                  1000
 IDC_BACKCOLORBOX               1001
 IDC_TEXTCOLORBOX               1002
 IDD_FINDDLG                    2000
 IDD_GOTODLG                    3000
 IDD_REPLACEDLG                 4000
 IDC_FINDEDIT                   1000
 IDC_MATCHCASE                  1001
 IDC_REPLACEEDIT                1001
 IDC_WHOLEWORD                  1002
 IDC_DOWN                       1003
 IDC_UP                         1004
 IDC_LINENO                     1005]

[RichEditID 300]

[ClassName:         B§ 'IczEditClass',0
 AppName :          B§ 'IczEdit version 1.0',0
 RichEditDLL:       B§ 'riched20.dll',0
 RichEditClass:     B§ 'RichEdit20A',0
 NoRichEdit:        B§ 'Cannot find riched20.dll',0
 ASMFilterString:   B§ 'ASM Source code (*.asm)',0,'*.asm',0
                    B§ 'All Files (*.*)',0,'*.*',0,0
 OpenFileFail:      B§ 'Cannot open the file',0
 WannaSave:         B§ 'The data in the control is modified. Want to save it?',0]
 
[FileOpened:        D§ &FALSE
 BackgroundColor:   D§ 0FFFFFF          ; default to white
 TextColor:         D§ 0]               ; default to black

[WordFileName: B§   '\wordfile.txt',0
 ASMSection:        'ASSEMBLY',0
 C1Key:             'C1',0
 C2Key:             'C2',0
 C3Key:             'C3',0
 C4Key:             'C4',0
 C5Key:             'C5',0
 C6Key:             'C6',0
 C7Key:             'C7',0
 C8Key:             'C8',0
 C9Key:             'C9',0
 C10Key:            'C10',0
 ZeroString:        0]

[ASMColorArray: D§ 0FF0000,0805F50,0FF,0666F00,044F0,05F8754,0FF0000,0FF0000,0FF0000,0FF0000
 CommentColor:  0808000]

[hInstance:         D§ ?
 hRichEdit:         D§ ?
 hwndRichEdit:      D§ ?]
[FileName:          B§ ? #260]
[AlternateFileName: B§ ? #260]
[CustomColors:      D§ ? #16]
[FindBuffer: B§ ? #260]
[ReplaceBuffer: B§ ? #260]
[uFlags: ?]
[FINDTEXT:
 @chrg: @chrg@cpMin: D§ 0
        @chrg@cpMax: D§ 0
 @lpstrText: D§ 0
 @chrgText:  @chrgText@cpMin: D§ 0
             @chrgText@cpMax: D§ 0]

[ASMSyntaxArray: D§ 0 #256  ASMSyntaxArrayLen: len]

[hMainHeap: ?  ; heap handle
 OldWndProc: ?
 RichEditVersion: ?]
____________________________________________________________________________________________
____________________________________________________________________________________________

[WNDCLASSEX:
 @cbSize: D§ len
 @style: D§ &CS_HREDRAW__&CS_VREDRAW
 @lpfnWndProc: D§ MainWindowProc
 @cbClsExtra: D§ 0
 @cbWndExtra: D§ 0
 @hInstance: D§ 0
 @hIcon: D§ 0
 @hCursor: D§ 0
 @hbrBackground: D§ &COLOR_WINDOW+1
 @lpszMenuName: D§ M00_Menu
 @lpszClassName: D§ ClassName
 @hIconSm: D§ 0]

[MSG:
 @hwnd: D§ 0
 @message: D§ 0
 @wParam: D§ 0
 @lParam: D§ 0
 @time: D§ 0
 @MSG@pt: @MSG@pt@x: D§ 0
          @MSG@pt@y: D§ 0]

[hwnd: D§ 0]

[M00_Menu  1000                  M00_Open  1001                  M00_Close  1002
 M00_Save  1003                  M00_save_As  1004               M00_Exit  1005
 M00_Undo  1006                  M00_Redo  1007                  M00_Copy  1008
 M00_Cut  1009                   M00_Paste  1010                 M00_Delete  1011
 M00_select_All  1012            M00_Find  1013                  M00_Find_Next  1014
 M00_Find_Prev  1015             M00_Replace  1016               M00_Go_To_Line  1017
 M00_Options  1018]

____________________________________________________________________________________________
____________________________________________________________________________________________

[&SCF_ALL 4]

[ACCELERATORS: 
 U§ &FVIRTKEY__&FNOINVERT                       &VK_F3  M00_Find_Next
    &FVIRTKEY__&FCONTROL__&FNOINVERT            &VK_F3  M00_Find_Prev
    
    &FVIRTKEY__&FCONTROL__&FNOINVERT            'F'     M00_Find
    &FVIRTKEY__&FCONTROL__&FNOINVERT            'G'     M00_Go_To_Line
    &FVIRTKEY__&FCONTROL__&FNOINVERT+FLAGLAST   'R'     M00_Replace]

[ACCELNUMBER 5   FLAGLAST 080]

[AccelHandle: 0    hSearch: 0]

____________________________________________________________________________________________
____________________________________________________________________________________________

 
Main:
    call 'KERNEL32.GetModuleHandleA'  &NULL
    mov D§hInstance eax, D§WNDCLASSEX@hInstance eax
    
    call 'KERNEL32.LoadLibraryA' RichEditDLL
    .If eax <> 0
        mov D§hRichEdit eax
        call 'KERNEL32.GetProcessHeap' | mov D§hMainHeap eax
  ;===========================================================
  ; Load the words to be hilighted
  ;===========================================================
        call FillHiliteInfo
    .Else
        call 'USER32.MessageBoxA' 0 NoRichEdit AppName &MB_OK__&MB_ICONERROR | jmp L9>>
    .End_If
    
    call 'USER32.LoadIconA' &NULL &IDI_APPLICATION
    mov D§WNDCLASSEX@hIcon eax, D§WNDCLASSEX@hIconSm eax
    call 'USER32.LoadCursorA' &NULL &IDC_ARROW
    mov D§WNDCLASSEX@hCursor eax

    call 'USER32.RegisterClassExA' WNDCLASSEX
    
    call 'USER32.CreateWindowExA' &NULL ClassName AppName,
                                  &WS_OVERLAPPEDWINDOW  &CW_USEDEFAULT,
                                  &CW_USEDEFAULT &CW_USEDEFAULT &CW_USEDEFAULT &NULL &NULL,
                                  D§hInstance &NULL
    mov   D§hwnd eax

    call 'USER32.ShowWindow' D§hwnd &SW_SHOWNORMAL
    call 'USER32.UpdateWindow' D§hwnd
    
    
    call 'USER32.CreateAcceleratorTableA' ACCELERATORS ACCELNUMBER
    mov D§AccelHandle eax
 
L1: call 'USER32.GetMessageA' MSG  0 0 0 | On eax = 0, jmp L8>

        call 'USER32.IsDialogMessage' D§hSearch MSG
        
        .If eax = 0
            call 'USER32.TranslateAccelerator' D§hwnd D§AccelHandle MSG
            If eax = 0
                call 'USER32.TranslateMessage' MSG
                call 'USER32.DispatchMessageA' MSG
            End_If
        
        .End_If
        
        jmp L1<
        
L8: call 'KERNEL32.FreeLibrary' D§hRichEdit
    call 'USER32.DestroyAcceleratorTable' D§AccelHandle
 
L9: call 'KERNEL32.ExitProcess' 0

____________________________________________________________________________________________
____________________________________________________________________________________________

Proc StreamInProc:
    arguments @hFile @pBuffer @NumBytes @pBytesRead

        call 'KERNEL32.ReadFile' D@hFile D@pBuffer D@NumBytes D@pBytesRead 0
        xor eax 1
EndP


Proc StreamOutProc:
    Arguments @hFile @pBuffer @NumBytes @pBytesWritten
    
        call 'KERNEL32.WriteFile' D@hFile D@pBuffer D@NumBytes D@pBytesWritten 0
        xor eax 1
EndP


Proc CheckModifyState:
    Argument @hWnd
    
        call 'USER32.SendMessageA' D§hwndRichEdit &EM_GETMODIFY 0 0
        .If eax <> 0
            call 'USER32.MessageBoxA' D§hWnd WannaSave AppName &MB_YESNOCANCEL
            If eax = &IDYES
                call 'USER32.SendMessageA' D@hWnd &WM_COMMAND M00_SAVE 0
            Else _If eax = &IDCANCEL
                mov eax &FALSE | Exit
            End_If
        .Endif
        mov eax &TRUE
EndP


[CHARFORMAT:
 @cbSize:           D§ 60
 @dwMask:           D§ &CFM_COLOR
 @dwEffects:        D§ 0  ; &CFE_AUTOCOLOR ; ?
 @yHeight:          D§ 0
 @yOffset:          D§ 0
 @crTextColor:      D§ 0
 @bCharSet:         B§ 0
 @bPitchAndFamily:  B§ 0]
[@szFaceName:       B§ 0 #32] ; &LF_FACESIZE
[wPad2:             W§ 0]


SetColor:
    call 'USER32.SendMessageA' D§hwndRichEdit &EM_SETBKGNDCOLOR 0 D§BackgroundColor

    move D§CHARFORMAT@crTextColor D§TextColor
    call 'USER32.SendMessageA' D§hwndRichEdit &EM_SETCHARFORMAT &SCF_ALL CHARFORMAT
ret


[CHOOSECOLOR:
 @lStructSize:      D§ len
 @hwndOwner:        D§ 0
 @hInstance:        D§ 0
 @rgbResult:        D§ 0
 @lpCustColors:     D§ CustomColors
 @Flags:            D§ &CC_RGBINIT
 @lCustData:        D§ 0
 @lpfnHook:         D§ 0
 @lpTemplateName:   D§ 0]

Proc OptionProc:
    Arguments @hWnd @uMsg @wParam @lParam
    
    pushad
    
        ...If D@uMsg = &WM_INITDIALOG
            ; returns &TRUE
        ...Else_If D@uMsg = &WM_COMMAND
            mov eax D@wParam | shr eax 16
            ..If ax = &BN_CLICKED
                mov eax D@wParam
                .If ax = &IDCANCEL
                    call 'USER32.SendMessageA' D@hWnd &WM_CLOSE 0 0
                    
                .Else_If ax = IDC_BACKCOLORBOX
                    move D§CHOOSECOLOR@hwndOwner D@hwnd
                    move D§ CHOOSECOLOR@hInstance D§hInstance
                    Move D§CHOOSECOLOR@rgbResult D§BackgroundColor
                    call 'COMDLG32.ChooseColorA' CHOOSECOLOR
                    If eax <> 0
                        move D§BackgroundColor D§CHOOSECOLOR@rgbResult
                        call 'USER32.GetDlgItem' D@hWnd IDC_BACKCOLORBOX
                        call 'USER32.InvalidateRect' eax 0 &TRUE
                    End_If
                    
                .Else_If ax = IDC_TEXTCOLORBOX
                    move D§CHOOSECOLOR@hwndOwner D@hwnd
                    move D§ CHOOSECOLOR@hInstance D§hInstance
                    move D§CHOOSECOLOR@rgbResult D§TextColor
                    call 'COMDLG32.ChooseColorA' CHOOSECOLOR
                    If eax <> 0
                        move D§TextColor D§CHOOSECOLOR@rgbResult
                        call 'USER32.GetDlgItem' D@hWnd IDC_TEXTCOLORBOX
                        call 'USER32.InvalidateRect' eax 0 &TRUE
                    End_If
                    
                .Else_If ax = &IDOK
    ;================================================================================
    ; Save the modify state of the richedit control because changing the text color changes the
    ; modify state of the richedit control.
    ;==================================================================================
                    call 'USER32.SendMessageA' D§hwndRichEdit &EM_GETMODIFY 0 0
                    push eax
                        call SetColor
                    pop eax
                    call 'USER32.SendMessageA' D§hwndRichEdit &EM_SETMODIFY eax 0
                    call 'USER32.EndDialog' D@hWnd 0
                .End_If
                
            ..End_If
                
        ...Else_If D@uMsg = &WM_CTLCOLORSTATIC
            call 'USER32.GetDlgItem' D@hWnd IDC_BACKCOLORBOX
            .If eax = D@lParam
                popad
                call 'GDI32.CreateSolidBrush' D§BackgroundColor 
                Exit
            .Else
                call 'USER32.GetDlgItem' D@hWnd IDC_TEXTCOLORBOX
                If eax = D@lParam
                    popad
                    call 'GDI32.CreateSolidBrush' D§TextColor
                    Exit
                End_If
            .End_If
            popad | mov eax &FALSE | Exit
            
        ...Else_If D@uMsg = &WM_CLOSE
            call 'USER32.EndDialog' D@hWnd 0
        ...Else
            popad | mov eax &FALSE | Exit
        ...End_If
        popad | mov eax &TRUE
EndP
____________________________________________________________________________________________
____________________________________________________________________________________________

Proc SearchProc:
    Arguments @hWnd, @uMsg, @wParam, @lParam
    
    pushad
    
    ...If D@uMsg = &WM_INITDIALOG
        move D§hSearch D@hWnd
  ;===================================================
  ; Select the default search down option
  ;===================================================
        call 'USER32.CheckRadioButton' D@hWnd IDC_DOWN IDC_UP IDC_DOWN
        call 'USER32.SendDlgItemMessageA' D@hWnd IDC_FINDEDIT &WM_SETTEXT 0 FindBuffer
        
    ...Else_If D@uMsg = &WM_COMMAND
        mov eax D@wParam | shr eax 16
        ..If ax = &BN_CLICKED
            mov eax D@wParam
            On ax = &IDCANCEL, call 'USER32.SendMessageA' D@hWnd &WM_CLOSE 0 0
            If ax <> &IDOK
                mov eax &FALSE | Exit
            End_If
                mov D§uFlags 0
                call 'USER32.SendMessageA' D§hwndRichEdit &EM_EXGETSEL 0 FINDTEXT@chrg
                call 'USER32.GetDlgItemTextA' D@hWnd IDC_FINDEDIT FindBuffer 260
                If eax = 0
                    mov eax &TRUE | Exit
                End_If
                    call 'USER32.IsDlgButtonChecked' D@hWnd IDC_DOWN
                    If eax = &BST_CHECKED
                        or D§uFlags &FR_DOWN
                        mov eax D§FINDTEXT@chrg@cpMin
                        On eax <> D§FINDTEXT@chrg@cpMax,
                            move D§FINDTEXT@chrg@cpMin D§FINDTEXT@chrg@cpMax
                        mov D§FINDTEXT@chrg@cpMax 0-1
                    Else
                        mov D§FINDTEXT@chrg@cpMax 0
                    End_If
                    call 'USER32.IsDlgButtonChecked' D@hWnd IDC_MATCHCASE
                    On eax = &BST_CHECKED, or D§uFlags &FR_MATCHCASE
                    call 'USER32.IsDlgButtonChecked' D@hWnd IDC_WHOLEWORD
                    On eax = &BST_CHECKED, or D§uFlags &FR_WHOLEWORD
                    mov D§FINDTEXT@lpstrText FindBuffer
                    call 'USER32.SendMessageA' D§hwndRichEdit &EM_FINDTEXTEX D§uFlags FINDTEXT
                    On eax <> 0-1,
                        call 'USER32.SendMessageA' D§hwndRichEdit &EM_EXSETSEL 0,
                                                   FINDTEXT@chrgText
      
        ..Else
           popad | mov eax &FALSE | Exit
        ..End_If
        
    ...Else_If D@uMsg = &WM_CLOSE
        mov D§hSearch 0
        call 'USER32.EndDialog' D@hWnd 0
        
    ...Else
        popad | mov eax &FALSE | Exit
        
    ...End_If
  
    popad | mov eax &TRUE
EndP


[&ST_SELECTION 2    &CP_ACP 0   &EM_SETTEXTEX 0461]

Proc ReplaceProc:
    Arguments @hWnd, @uMsg, @wParam, @lParam
    Structure @SETTEXT 8, @SETTEXT.flags 0, @SETTEXT.codepage 4
    
    ...If D@uMsg = &WM_INITDIALOG
        move D§hSearch D@hwnd
        call 'USER32.SetDlgItemTextA' D@hWnd IDC_FINDEDIT FindBuffer
        call 'USER32.SetDlgItemTextA' D@hWnd IDC_REPLACEEDIT ReplaceBuffer
        call 'USER32.GetDlgItem' D@hWnd IDC_REPLACEEDIT
        call 'USER32.SetFocus' eax
        
    ...Else_If D@uMsg = &WM_COMMAND
        mov eax D@wParam | shr eax 16
        ..If ax = &BN_CLICKED
            mov eax D@wParam
            .If ax = &IDCANCEL
                call 'USER32.SendMessageA' D@hWnd &WM_CLOSE 0 0
                
            .Else_If ax = &IDOK
                call 'USER32.GetDlgItemTextA' D@hWnd IDC_FINDEDIT FindBuffer 260
                call 'USER32.GetDlgItemTextA' D@hWnd IDC_REPLACEEDIT ReplaceBuffer 260
                mov D§FINDTEXT@chrg@cpMin 0
                mov D§FINDTEXT@chrg@cpMax 0-1
                mov D§FINDTEXT@lpstrText FindBuffer
                mov D@SETTEXT.flags &ST_SELECTION
                mov D@SETTEXT.codepage &CP_ACP
 
L1:             call 'USER32.SendMessageA' D§hwndRichEdit &EM_FINDTEXTEX &FR_DOWN FINDTEXT
                On eax = 0-1, jp L2>
                call 'USER32.SendMessageA' D§hwndRichEdit &EM_EXSETSEL 0 FINDTEXT@chrgText
                call 'USER32.SendMessageA' D§hwndRichEdit &EM_SETTEXTEX D@SETTEXT ReplaceBuffer
                jmp L1<
L2:                
            .End_If
        ..End_If
        
    ...Else_If D@uMsg = &WM_CLOSE
        mov D§hSearch 0 | call 'USER32.EndDialog' D@hWnd 0
        
    ...Else
        mov eax &FALSE | Exit

    ...End_If
    mov eax &TRUE
EndP



Proc GoToProc:
    Arguments @hWnd, @uMsg, @wParam, @lParam
    LOCAL @LineNo
    
    ...If D@uMsg = &WM_INITDIALOG
        move D§hSearch D@hwnd
        
    ...Else_If D@uMsg = &WM_COMMAND
        mov eax D@wParam | shr eax 16
        ..If ax = &BN_CLICKED
            mov eax D@wParam
            .If ax = &IDCANCEL
                call 'USER32.SendMessageA' D@hWnd &WM_CLOSE 0 0
            .Else_If ax = &IDOK
                call 'USER32.GetDlgItemInt' D@hWnd IDC_LINENO &NULL &FALSE
                mov D@LineNo eax
                call 'USER32.SendMessageA' D§hwndRichEdit &EM_GETLINECOUNT 0 0
                If eax > D@LineNo
                    call 'USER32.SendMessageA' D§hwndRichEdit &EM_LINEINDEX D@LineNo 0
                    call 'USER32.SendMessageA' D§hwndRichEdit &EM_SETSEL eax eax
                    call 'USER32.SetFocus' D§hwndRichEdit
                End_If
            .End_If
        ..End_If
        
    ...Else_If D@uMsg = &WM_CLOSE
        mov D§hSearch 0 | call 'USER32.EndDialog' D@hWnd 0
        
    ...Else
        mov eax &FALSE | Exit
 
    ...End_If
    mov eax &TRUE
EndP



Proc PrepareEditMenu:
    Argument @hSubMenu
    Structure @CHARRANGE 8, @CHARRANGE.cpMin 0, @CHARRANGE.cpMax 4

 ;=============================================================================
 ; Check whether there is some text in the clipboard. If so, we enable the paste menuitem
 ;=============================================================================
    call 'USER32.SendMessageA' D§hwndRichEdit &EM_CANPASTE &CF_TEXT 0
    If eax = 0  ; no text in the clipboard
        call 'USER32.EnableMenuItem' D@hSubMenu M00_PASTE &MF_GRAYED
    Else
        call 'USER32.EnableMenuItem' D@hSubMenu M00_PASTE &MF_ENABLED
    End_If
 ;==========================================================
 ; check whether the undo queue is empty
 ;==========================================================
    call 'USER32.SendMessageA' D§hwndRichEdit &EM_CANUNDO 0 0
    If eax = 0
        call 'USER32.EnableMenuItem' D@hSubMenu M00_UNDO &MF_GRAYED
    Else
        call 'USER32.EnableMenuItem' D@hSubMenu M00_UNDO &MF_ENABLED
    End_If
 ;=========================================================
 ; check whether the redo queue is empty
 ;=========================================================
    call 'USER32.SendMessageA' D§hwndRichEdit &EM_CANREDO 0 0
    If eax = 0
        call 'USER32.EnableMenuItem' D@hSubMenu M00_REDO &MF_GRAYED
    Else
        call 'USER32.EnableMenuItem' D@hSubMenu M00_REDO &MF_ENABLED
    End_If
 ;=========================================================
 ; check whether there is a current selection in the richedit control.
 ; If there is, we enable the cut/copy/delete menuitem
 ;=========================================================
    call 'USER32.SendMessageA' D§hwndRichEdit &EM_EXGETSEL 0 D@CHARRANGE
    mov eax D@CHARRANGE.cpMin
    If eax = D@CHARRANGE.cpMax  ; no current selection
        call 'USER32.EnableMenuItem' D@hSubMenu M00_COPY &MF_GRAYED
        call 'USER32.EnableMenuItem' D@hSubMenu M00_CUT &MF_GRAYED
        call 'USER32.EnableMenuItem' D@hSubMenu M00_DELETE &MF_GRAYED
    Else
        call 'USER32.EnableMenuItem' D@hSubMenu M00_COPY &MF_ENABLED
        call 'USER32.EnableMenuItem' D@hSubMenu M00_CUT &MF_ENABLED
        call 'USER32.EnableMenuItem' D@hSubMenu M00_DELETE &MF_ENABLED
    End_If
EndP
____________________________________________________________________________________________
____________________________________________________________________________________________
[ParsBuffer: B§ ? #128]

Proc ParseBuffer:
    Arguments @hHeap, @pBuffer, @nSize, @ArrayOffset, @pArray
    Local @InProgress
    Uses edi esi
    
        mov D@InProgress &FALSE
        mov esi ParsBuffer, edi D@pBuffer
        call 'USER32.CharLowerA' edi
        mov ecx D@nSize | or ecx ecx | jz L9>>
        
L0:     cmp B§edi ' ' | je L2>
        cmp B§edi 9 | je L2>                ; tab
            mov D@InProgress &TRUE
            mov al B§edi, B§esi al | inc esi
L1:         inc edi | dec ecx | jz L9>>
                jmp L0<

L2:     cmp D@InProgress &TRUE | jne L1<

L9:     mov B§esi 0                         ; Found
        push ecx
 ;========================================================
 ; store the word in a WORDINFO structure
 ;========================================================
            call 'KERNEL32.HeapAlloc' D@hHeap &HEAP_ZERO_MEMORY WordInfoLen
            push esi
                mov esi eax
                call 'KERNEL32.lstrlen' ParsBuffer
                mov D§esi+WORDINFO.WordLen eax
                move D§esi+WORDINFO.pColor D@ArrayOffset
                inc eax
                call 'KERNEL32.HeapAlloc' D@hHeap &HEAP_ZERO_MEMORY eax
                mov D§esi+WORDINFO.pszWord eax, edx eax
                call 'KERNEL32.lstrcpy' edx ParsBuffer
                mov eax D@pArray
                movzx edx B§ParsBuffer | shl edx 2  ; multiply by 4
                add eax edx
                If D§eax = 0
                    mov D§eax esi
                Else
                    move D§esi+WORDINFO.NextLink D§eax
                    mov D§eax esi
                End_If
            pop esi
        pop ecx
        mov esi ParsBuffer, D@InProgress &FALSE
        cmp ecx 0 | ja L1<<
EndP
____________________________________________________________________________________________
[HiliteBuffer: B§ ? #1024]

Proc FillHiliteInfo:
    LOCAL @pTemp, @BlockSize
    
    pushad
 ;===================================================================
 ; Zero out the array
 ;===================================================================
        call 'KERNEL32.RtlZeroMemory' ASMSyntaxArray D§ASMSyntaxArrayLen
 ;===================================================================
 ; obtaining the path of this program instance
 ;===================================================================
        call 'KERNEL32.GetModuleFileNameA' D§hInstance HiliteBuffer 1024
        call 'KERNEL32.lstrlen' HiliteBuffer
        mov ecx eax | dec ecx
        mov edi HiliteBuffer | add edi ecx
        std
            mov al '\'
            repne scasb
        cld
        inc edi | mov B§edi 0
        call 'KERNEL32.lstrcat' HiliteBuffer WordFileName
 ;==================================================================
 ; Check whether the file exists
 ;==================================================================
        call 'KERNEL32.GetFileAttributesA' HiliteBuffer
        ...If eax <> 0-1
  ;===================================================================
  ; allocate a block of memory from the heap for the strings
  ;===================================================================
            mov D@BlockSize 10240
            call 'KERNEL32.HeapAlloc' D§hMainHeap 0 D@BlockSize
            mov D@pTemp,eax
            FillHiliteFromProfile C1Key  0, C2Key  4, C3Key  8, C4Key  12, C5Key 16, 
                                  C6Key 20, C7Key 24, C8Key 28, C9Key 32, C10Key 36

            call 'KERNEL32.HeapFree' D§hMainHeap 0 D@pTemp
        ...End_If
    popad
EndP


[FillHiliteFromProfile | L1:
    call 'KERNEL32.GetPrivateProfileStringA' ASMSection #1 ZeroString,
                                             D@pTemp D@BlockSize HiliteBuffer
    .If eax <> 0
        inc eax
        If eax = D§FillHiliteInfo@BlockSize         ; the buffer is too small
            add D§FillHiliteInfo@BlockSize 10240
            call 'KERNEL32.HeapReAlloc' D§hMainHeap 0 D§FillHiliteInfo@pTemp D§FillHiliteInfo@BlockSize
            mov D§FillHiliteInfo@pTemp eax
            jmp L1<
         End_If
         mov edx ASMColorArray | add edx #2
         call ParseBuffer D§hMainHeap D§FillHiliteInfo@pTemp eax edx ASMSyntaxArray
    .endif
#+2]
____________________________________________________________________________________________

[NewBuffer: B§ ? #10240]

[txtrange:
 @chrg: @cpMin: D§ 0
        @cpMax: D§ 0
 @lpstrText: D§ 0]

[VirtRECT: @left: 0  @top: 0  @right: 0  @bottom: 0]

[RealRECT: @left: 0  @top: 0  @right: 0  @bottom: 0]

[POINT: @x: 0  @y: 0]

Proc NewRichEditProc:
    Arguments @hWnd, @uMsg, @wParam, @lParam
    Local @hdc, @hOldFont, @FirstChar, @hRgn, @hOldRgn, @pString, @BufferSize

    ...If D@uMsg = &WM_PAINT
        push edi
        push esi
        call 'USER32.HideCaret' D@hWnd
        call 'USER32.CallWindowProcA' D§OldWndProc D@hWnd D@uMsg D@wParam D@lParam
        push eax
        mov edi ASMSyntaxArray
        call 'USER32.GetDC' D@hWnd | mov D@hdc eax
        call 'GDI32.SetBkMode' D@hdc &TRANSPARENT
  ;===================================================================
  ; Do syntax hiliting here!
  ;===================================================================
        call 'USER32.SendMessageA' D@hWnd &EM_GETRECT 0 VirtRECT
        call 'USER32.SendMessageA' D@hWnd &EM_CHARFROMPOS 0 VirtRECT
  ;========================================================
  ; obtain the line number
  ;========================================================
        call 'USER32.SendMessageA' D@hWnd &EM_LINEFROMCHAR eax 0
        call 'USER32.SendMessageA' D@hWnd &EM_LINEINDEX eax 0
        mov D§txtrange@cpMin eax
        mov D@FirstChar eax
        call 'USER32.SendMessageA' D@hWnd &EM_CHARFROMPOS 0 VirtRECT@right
        mov D§txtrange@cpMax eax
        move D§RealRect@left D§VirtRECT@left
        move D§RealRect@top D§VirtRECT@top
        move D§RealRect@right D§VirtRECT@right
        move D§RealRect@bottom D§VirtRECT@bottom
        call 'GDI32.CreateRectRgn' D§RealRect@left D§RealRect@top D§RealRect@right,
                                    D§RealRect@bottom
        mov D@hRgn eax
        call 'GDI32.SelectObject' D@hdc D@hRgn
        mov D@hOldRgn eax
        call 'GDI32.SetTextColor' D@hdc D§CommentColor
  ;===================================================================
  ; Get the visible text into buffer
  ;===================================================================
        mov D§txtrange@lpstrText NewBuffer
        call 'USER32.SendMessageA' D@hWnd &EM_GETTEXTRANGE 0 txtrange
        mov esi eax  ; esi == size of the text 
        ..If esi > 0
            mov D@BufferSize eax
   ;=========================================================
   ; Search for comments first
   ;=========================================================
            push edi
            push ebx
            mov edi NewBuffer
            mov edx edi  ; used as the reference point
            mov ecx esi
            mov al ';'

L0:         repne scasb | jne L2>>

            dec edi | inc ecx
            mov D@pString edi
            mov ebx edi | sub ebx edx
            add ebx D@FirstChar
            mov D§txtrange@cpMin ebx
   ;===================================================
   ; search the end of line or the end of buffer
   ;===================================================
            push eax
                mov al 0D
                repne scasb
            pop eax
; HiliteTheComment:
            On ecx > 0, mov B§edi-1 0
            mov ebx edi
            sub ebx edx
            add ebx D@FirstChar
            mov D§txtrange@cpMax ebx
            pushad
   ;====================================================================
   ; Now we must search the range for the tabs
   ;====================================================================
                mov edi D@pString
                mov esi D§txtrange@cpMax
                sub esi D§txtrange@cpMin  ; esi contains the length of the buffer
                mov eax esi
                push edi
                While eax > 0
                    On B§edi = 9, mov B§edi 0
                    inc edi | dec eax
                End_While
                pop edi
                .while esi > 0
                    .If B§edi <> 0
                        call 'KERNEL32.lstrlen' edi
                        push eax
                            mov ecx edi
                            mov edx NewBuffer
                            sub ecx edx
                            add ecx D@FirstChar
                            If D§RichEditVersion = 3
                                call 'USER32.SendMessageA' D@hWnd &EM_POSFROMCHAR VirtRECT ecx
                            Else
                                call 'USER32.SendMessageA' D@hWnd &EM_POSFROMCHAR ecx 0
                                mov ecx eax
                                and ecx 0FFFF
                                mov D§VirtRECT@left ecx
                                shr eax 16
                                mov D§VirtRECT@top eax
                            End_If
                            call 'USER32.DrawTextA' D@hdc edi 0-1 VirtRECT 0
                        pop eax
                        add edi eax | sub esi eax
                    .Else
                        inc edi | dec esi
                    .End_If   
                .End_While
                mov ecx D§txtrange@cpMax
                sub ecx D§txtrange@cpMin
                call 'KERNEL32.RtlZeroMemory' D@pString ecx
            popad
            On ecx > 0, jmp L0<<

L2:
            pop ebx
            pop edi
   ;==============================================================================
   ; Now that the comments are out of our way, Get rid of the separators
   ;==============================================================================
            mov ecx D@BufferSize
            mov esi NewBuffer
            .While ecx > 0
                mov al B§esi
                On al = ' ', jmp L2>
                On al = 0D, jmp L2>
                On al = '/', jmp L2>
                On al = ',', jmp L2>
                On al = '|', jmp L2>
                On al = '+', jmp L2>
                On al = '-', jmp L2>
                On al = '*', jmp L2>
                On al = '&', jmp L2>
                On al = '<', jmp L2>
                On al = '>', jmp L2>
                On al = '=', jmp L2>
                On al = '(', jmp L2>
                On al = ')', jmp L2>
                On al = '{', jmp L2>
                On al = '}', jmp L2>
                On al = '[', jmp L2>
                On al = ']', jmp L2>
                On al = '^', jmp L2>
                On al = ':', jmp L2>
                On al = 9, jmp L2>
                jmp L3>
L2:             mov B§esi 0
L3:             dec ecx
                inc esi
            .End_While
   ;============================================================================
   ; Begin the search 
   ;============================================================================
            mov esi NewBuffer
            mov ecx D@BufferSize
            ..While ecx > 0
                mov al B§esi
                .If al <> 0
                    push ecx
                    call 'KERNEL32.lstrlen' esi
                    push eax
                        mov edx eax  ; edx contains the length of the string
                        movzx eax B§esi
                        On al < 'A', jmp L2>
                        On al > 'Z', jmp L2>
                            add al 020
L2:                     shl eax 2
                        add eax edi  ; edi contains the pointer to the WORDINFO pointer array
                        On D§eax = 0, jmp L7>>
                            mov eax D§eax
                       ; assume eax:ptr WORDINFO
                            .While eax <> 0
                                On edx <> D§eax+WORDINFO.WordLen, jmp L6>>
                                    pushad
                                        call 'KERNEL32.lstrcmpi' D§eax+WORDINFO.pszWord esi
                                        On eax <> 0, jmp L5>>
                                            popad
         ;=================================================
         ; hilite the word
         ;=================================================
                                            mov ecx esi
                                            mov edx NewBuffer
                                            sub ecx edx
                                            add ecx D@FirstChar
                                            pushad
                                                If D§RichEditVersion = 3
                                                    call 'USER32.SendMessageA' D@hWnd &EM_POSFROMCHAR,
                                                                            VirtRECT ecx
                                                Else
                                                    call 'USER32.SendMessageA' D@hWnd &EM_POSFROMCHAR,
                                                                            ecx,0
                                                    mov ecx eax | and ecx 0FFFF | mov D§VirtRECT@left ecx
                                                    shr eax 16 | mov D§VirtRECT@top eax
                                                End_If
                                            popad
                                            mov edx D§eax+WORDINFO.pColor
                                            call 'GDI32.SetTextColor' D@hdc D§edx
                                            call 'USER32.DrawTextA' D@hdc esi 0-1 VirtRECT 0
                          jmp L7>>         ; !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .break
L5:                                   ; .End_If
                                    popad
L6:                           ; .End_If
                                mov eax D§eax+WORDINFO.NextLink
                            .End_While
                      ; .End_If
L7:                 pop eax
                    pop ecx
                    add esi eax | sub ecx eax
                .Else
                    inc esi | dec ecx
                .End_If
            ..End_While
        ..End_If
   
        call 'GDI32.SelectObject' D@hdc D@hOldRgn
        call 'GDI32.DeleteObject' D@hRgn
        call 'GDI32.SelectObject' D@hdc D@hOldFont
        call 'USER32.ReleaseDC' D@hWnd D@hdc
        call 'USER32.ShowCaret' D@hWnd
        pop eax
        pop esi
        pop edi
    ...Else_If D@uMsg = &WM_CLOSE
        call 'USER32.SetWindowLongA' D@hWnd &GWL_WNDPROC D§OldWndProc  
    ...Else
        call 'USER32.CallWindowProcA' D§OldWndProc D@hWnd D@uMsg D@wParam D@lParam
    ...End_If
EndP

____________________________________________________________________________________________
____________________________________________________________________________________________

[CHARRANGE:
 @cpMin: D§ 0
 @cpMax: D§ 0]

[OPENFILENAME:
 @lStructSize: D§ Len
 @hWndOwner: D§ 0
 @hInstance: D§ 0
 @lpstrFilter: D§ ASMFilterString
 @lpstrCustomFilter: D§ 0
 @nMaxCustFilter: D§ 0
 @nFilterIndex: D§ 0
 @lpstrFile: D§ FileName
 @nMaxFile: D§ 260
 @lpstrFileTitle: D§ 0
 @nMaxFileTitle: D§ 260
 @lpstrInitialDir: D§ 0
 @lpstrTitle: D§ 0
 @Flags: D§ 0
 @nFileOffset: W§ 0
 @nFileExtension: W§ 0
 @lpstrDefExt: D§ 0
 @lCustData: D§ 0
 @lpfnHook: D§ 0
 @lpTemplateName: D§ 0]

[Buffer: B§ ? #256]

[EDITSTREAM:
 @dwCookie: D§ 0
 @dwError: D§ 0
 @pfnCallback: D§ 0]

[hFile: 0       hPopup: 0]

[Pt: Pt.X: 0    Pt.Y: 0]

[&TO_SIMPLELINEBREAK 2]


Proc MainWindowProc:
    Arguments @hWnd @uMsg @wParam @lParam
    
    pushad

    ...If D@uMsg = &WM_CREATE
        call 'USER32.CreateWindowExA' &WS_EX_CLIENTEDGE RichEditClass 0,
               &WS_CHILD__&WS_VISIBLE__&ES_MULTILINE__&WS_VSCROLL__&WS_HSCROLL__&ES_NOHIDESEL,
               &CW_USEDEFAULT &CW_USEDEFAULT &CW_USEDEFAULT &CW_USEDEFAULT D@hWnd,
               RichEditID D§hInstance 0
        mov D§hwndRichEdit eax

  ;=======================================================
  ; Check the richedit version
  ;=======================================================
        call 'USER32.SendMessageA' D§hwndRichEdit &EM_SETTYPOGRAPHYOPTIONS,
                                   &TO_SIMPLELINEBREAK &TO_SIMPLELINEBREAK
        call 'USER32.SendMessageA' D§hwndRichEdit &EM_GETTYPOGRAPHYOPTIONS 1 1
        If eax = 0  ; means this message is not processed
            mov D§RichEditVersion 2
        Else
            mov D§RichEditVersion 3
   ;=============================================================================
   ; Make it emulate system edit control so the text color update doesn't  take very long
   ;=============================================================================
            call 'USER32.SendMessageA' D§hwndRichEdit &EM_SETEDITSTYLE,
                                       &SES_EMULATESYSEDIT &SES_EMULATESYSEDIT
        End_If
  ;=======================================================
  ; Subclass the richedit control
  ;=======================================================
        call 'USER32.SetWindowLongA' D§hwndRichEdit &GWL_WNDPROC NewRichEditProc
        mov D§OldWndProc eax
  ;=============================================================
  ; Set the text limit. The default is 64K
  ;=============================================================
        call 'USER32.SendMessageA' D§hwndRichEdit &EM_LIMITTEXT 0-1 0
  ;=============================================================
  ; Set the default text/background color
  ;=============================================================
        call SetColor
        call 'USER32.SendMessageA' D§hwndRichEdit &EM_SETMODIFY &FALSE 0
  ;============================================================
  ; set event mask
  ;============================================================
        call 'USER32.SendMessageA' D§hwndRichEdit &EM_SETEVENTMASK 0 &ENM_MOUSEEVENTS
        call 'USER32.SendMessageA' D§hwndRichEdit &EM_EMPTYUNDOBUFFER 0 0
        
    ...Else_If D@uMsg = &WM_NOTIFY
        ;[MSGFILTER:
        ; @nmhdr: @NMHDR@hwndFrom: D§ 0 ; +0
        ;         @NMHDR@idfrom: D§ 0   ; +4
        ;         @NMHDR@code: D§ 0     ; +8
        ; @msg: D§ 0                    ; +12
        ; @wParam: D§ 0                 ; +16
        ; @lParam: D§ 0]                ; +20
        push esi
            mov esi D@lParam
            .If D§esi+8 = &EN_MSGFILTER
                .If D§esi+12 = &WM_RBUTTONDOWN
                    call 'USER32.GetMenu' D@hWnd
                    call 'USER32.GetSubMenu' eax 1 | mov D§hPopup eax
                    call PrepareEditMenu D§hPopup
                    mov edx D§esi+20, ecx edx
                    and edx 0FFFF | shr ecx 16
                    mov D§Pt.X edx, D§Pt.Y ecx
                    call 'USER32.ClientToScreen' D@hWnd Pt
                    call 'USER32.TrackPopupMenu' D§hPopup &TPM_LEFTALIGN__&TPM_BOTTOMALIGN,
                                                 D§Pt.X D§Pt.Y &NULL D@hWnd &NULL
                .End_If
            .End_If
        pop esi
  
    ...Else_If D@uMsg = &WM_INITMENUPOPUP
        mov eax D@lParam
        ..If ax = 0  ; file menu   
            .If D§FileOpened = &TRUE ; a file is already opened
                call 'USER32.EnableMenuItem' D@wParam M00_OPEN &MF_GRAYED
                call 'USER32.EnableMenuItem' D@wParam M00_CLOSE &MF_ENABLED
                call 'USER32.EnableMenuItem' D@wParam M00_SAVE &MF_ENABLED
                call 'USER32.EnableMenuItem' D@wParam M00_SAVEAS &MF_ENABLED
            .Else
                call 'USER32.EnableMenuItem' D@wParam M00_OPEN &MF_ENABLED
                call 'USER32.EnableMenuItem' D@wParam M00_CLOSE &MF_GRAYED
                call 'USER32.EnableMenuItem' D@wParam M00_SAVE &MF_GRAYED
                call 'USER32.EnableMenuItem' D@wParam M00_SAVEAS &MF_GRAYED
            .End_If
 
        ..Else_If ax = 1 ; edit menu
            call PrepareEditMenu D@wParam
            
        ..Else_If ax = 2  ; search menu bar
            .If B§FileOpened = &TRUE
                call 'USER32.EnableMenuItem' D@wParam M00_FIND &MF_ENABLED
                call 'USER32.EnableMenuItem' D@wParam M00_FINDNEXT &MF_ENABLED
                call 'USER32.EnableMenuItem' D@wParam M00_FINDPREV &MF_ENABLED
                call 'USER32.EnableMenuItem' D@wParam M00_REPLACE &MF_ENABLED
                call 'USER32.EnableMenuItem' D@wParam M00_GOTOLINE &MF_ENABLED
            .Else
                call 'USER32.EnableMenuItem' D@wParam M00_FIND &MF_GRAYED
                call 'USER32.EnableMenuItem' D@wParam M00_FINDNEXT &MF_GRAYED
                call 'USER32.EnableMenuItem' D@wParam M00_FINDPREV &MF_GRAYED
                call 'USER32.EnableMenuItem' D@wParam M00_REPLACE &MF_GRAYED
                call 'USER32.EnableMenuItem' D@wParam M00_GOTOLINE &MF_GRAYED
            .End_If

        ..End_If
        
    ...Else_If D@uMsg = &WM_COMMAND
    
        ..If D@lParam = 0  ; menu commands
            mov eax D@wParam
            .If ax = M00_OPEN
                call OpenCommand D@hWnd

            .Else_If ax = M00_CLOSE
                call CheckModifyState D@hWnd
                If eax = &TRUE
                    call 'USER32.SetWindowTextA' D§hwndRichEdit 0
                    mov D§FileOpened &FALSE
                End_If
                
            .Else_If ax = M00_SAVE
                call 'KERNEL32.CreateFileA' FileName &GENERIC_WRITE &FILE_SHARE_READ,
                                           &NULL &CREATE_ALWAYS &FILE_ATTRIBUTE_NORMAL 0
                If eax <> &INVALID_HANDLE_VALUE
                    call InvalidFile

                Else
                    call 'USER32.MessageBoxA' D@hWnd OpenFileFail AppName,
                                              &MB_OK__&MB_ICONERROR
                End_If
                
            .Else_If ax = M00_COPY
                call 'USER32.SendMessageA' D§hwndRichEdit &WM_COPY 0 0
                
            .Else_If ax = M00_CUT
                call 'USER32.SendMessageA' D§hwndRichEdit &WM_CUT 0 0
                
            .Else_If ax = M00_PASTE
                call 'USER32.SendMessageA' D§hwndRichEdit &WM_PASTE 0 0
                
            .Else_If ax = M00_DELETE
                call 'USER32.SendMessageA' D§hwndRichEdit &EM_REPLACESEL &TRUE 0
                
            .Else_If ax = M00_SELECTALL
                mov D§CHARRANGE@cpMin 0
                mov D§CHARRANGE@cpMax 0-1
                call 'USER32.SendMessageA' D§hwndRichEdit &EM_EXSETSEL 0 CHARRANGE
                
            .Else_If ax = M00_UNDO
                call 'USER32.SendMessageA' D§hwndRichEdit &EM_UNDO 0 0
            
            .Else_If ax = M00_REDO
                call 'USER32.SendMessageA' D§hwndRichEdit &EM_REDO 0 0
                
            .Else_If ax = M00_OPTIONS
                call 'USER32.DialogBoxParamA' D§hInstance IDD_OPTIONDLG D@hWnd OptionProc 0
                
            .Else_If ax = M00_SAVEAS
                move D§OPENFILENAME@hwndOwner D@hwnd
                move D§OPENFILENAME@hInstance D§hInstance
                mov D§OPENFILENAME@lpstrFilter ASMFilterString
                mov D§OPENFILENAME@lpstrFile AlternateFileName
                mov D§AlternateFileName 0
                mov D§OPENFILENAME@Flags &OFN_FILEMUSTEXIST__&OFN_HIDEREADONLY__&OFN_PATHMUSTEXIST
                call 'COMDLG32.GetSaveFileNameA' OPENFILENAME
                If eax <> 0
                    call 'KERNEL32.CreateFileA' AlternateFileName &GENERIC_WRITE,
                                                &FILE_SHARE_READ &NULL &CREATE_ALWAYS,
                                                &FILE_ATTRIBUTE_NORMAL 0
                    On eax <> &INVALID_HANDLE_VALUE, call InvalidFile
                End_If
    
            .Else_If ax = M00_FIND
                On D§hSearch = 0,
                    call 'USER32.CreateDialogParamA' D§hInstance IDD_FINDDLG D@hWnd SearchProc 0
                    call 'USER32.GetDlgItem' eax IDC_FINDEDIT
                    call 'USER32.SetFocus' eax

            .Else_If ax = M00_REPLACE
                On D§hSearch = 0,
                    call 'USER32.CreateDialogParamA' D§hInstance IDD_REPLACEDLG D@hWnd,
                                                     ReplaceProc 0
                    call 'USER32.GetDlgItem' eax IDC_FINDEDIT
                    call 'USER32.SetFocus' eax
   
            .Else_If ax = M00_GO_TO_LINE
                On D§hSearch = 0
                    call 'USER32.CreateDialogParamA' D§hInstance IDD_GOTODLG D@hWnd GoToProc 0
                    call 'USER32.GetDlgItem' eax IDC_LINENO
                    call 'USER32.SetFocus' eax

            .Else_If ax = M00_FIND_NEXT
                call 'KERNEL32.lstrlen' FindBuffer
                If eax = 0
                    popad | mov eax &FALSE | Exit
                End_If
                    call 'USER32.SendMessageA' D§hwndRichEdit &EM_EXGETSEL 0 FINDTEXT@chrg
                    mov eax D§FINDTEXT@chrg@cpMin
                    On eax <> D§FINDTEXT@chrg@cpMax,
                        move D§FINDTEXT@chrg@cpMin, D§FINDTEXT@chrg@cpMax
                    mov D§FINDTEXT@chrg@cpMax 0-1
                    mov D§FINDTEXT@lpstrText FindBuffer
                    call 'USER32.SendMessageA' D§hwndRichEdit &EM_FINDTEXTEX &FR_DOWN,
                                               findtext
                    On eax <> 0-1,
                        call 'USER32.SendMessageA' D§hwndRichEdit &EM_EXSETSEL 0,
                                                   FINDTEXT@chrgText
                                                   
            .Else_If ax = M00_FIND_PREV
                call 'KERNEL32.lstrlen' FindBuffer
                If eax <> 0
                    call 'USER32.SendMessageA' D§hwndRichEdit &EM_EXGETSEL 0 FINDTEXT@chrg
                    mov D§FINDTEXT@chrg@cpMax 0
                    mov D§FINDTEXT@lpstrText FindBuffer
                    call 'USER32.SendMessageA' D§hwndRichEdit &EM_FINDTEXTEX 0 findtext
                    On eax <> 0-1,
                        call 'USER32.SendMessageA' D§hwndRichEdit &EM_EXSETSEL 0,
                                                   FINDTEXT@chrgText
                End_If
            
            .Else_If ax = M00_EXIT
                call 'USER32.SendMessageA' D@hWnd &WM_CLOSE 0 0
            .End_If
        ..End_If
        
    ...Else_If D@uMsg = &WM_CLOSE
        call CheckModifyState D@hWnd
        On eax = &TRUE, call 'USER32.DestroyWindow' D@hWnd
        
    ...Else_If D@uMsg = &WM_SIZE
        mov eax D@lParam, edx eax
        and eax 0FFFF | shr edx 16
        call 'USER32.MoveWindow' D§hwndRichEdit 0 0 eax edx &TRUE  
        
    ...Else_If D@uMsg = &WM_DESTROY
        call 'USER32.PostQuitMessage' &NULL
        
    ...Else
        popad
        call 'USER32.DefWindowProcA' D@hWnd D@uMsg D@wParam D@lParam  
        Exit
        
    ...End_If
    
    popad | mov eax &FALSE
EndP


InvalidFile:
    mov D§hFile eax
     _____________________________
     ; stream the text to the file
     _____________________________
    mov D§EDITSTREAM@dwCookie eax
    mov D§EDITSTREAM@pfnCallback StreamOutProc
    call 'USER32.SendMessageA' D§hwndRichEdit &EM_STREAMOUT &SF_TEXT EDITSTREAM
     ______________________________________
     ; Initialize the modify state to false
     ______________________________________
    call 'USER32.SendMessageA' D§hwndRichEdit &EM_SETMODIFY &FALSE 0
    call 'KERNEL32.CloseHandle' D§hFile
ret


Proc OpenCommand:
    Argument @hWnd

    move D§OPENFILENAME@hwndOwner D@hWnd
    move D§OPENFILENAME@hInstance D§hInstance
    mov D§FileName 0
    mov D§OPENFILENAME@Flags &OFN_FILEMUSTEXIST__&OFN_HIDEREADONLY__&OFN_PATHMUSTEXIST
    call 'COMDLG32.GetOpenFileNameA' OPENFILENAME
    
    ...If eax <> 0
        call 'KERNEL32.CreateFileA' FileName &GENERIC_READ,
                                    &FILE_SHARE_READ &NULL,                                    
                                    &OPEN_EXISTING  &FILE_ATTRIBUTE_NORMAL 0
        .If eax <> &INVALID_HANDLE_VALUE
            mov D§hFile eax
      ___________________________________________
      ; stream the text into the richedit control
      ___________________________________________
            mov D§EDITSTREAM@dwCookie eax
            mov D§EDITSTREAM@pfnCallback StreamInProc
            call 'USER32.SendMessageA' D§hwndRichEdit &EM_STREAMIN,
                                       &SF_TEXT EDITSTREAM
      ______________________________________
      ; Initialize the modify state to false
      ______________________________________
            call 'USER32.SendMessageA' D§hwndRichEdit &EM_SETMODIFY &FALSE 0
            call 'KERNEL32.CloseHandle' D§hFile
            mov B§FileOpened &TRUE
            
        .Else
            call 'USER32.MessageBoxA' D@hWnd OpenFileFail AppName,
                                      &MB_OK__&MB_ICONERROR
                                      
        .End_If
    ...End_If
EndP

Analysis:

The first action before calling WinMain to to call FillHiliteInfo. This function reads the content of wordfile.txt and parses the content.
Proc FillHiliteInfo:
    LOCAL @pTemp, @BlockSize
    
    pushad

        call 'KERNEL32.RtlZeroMemory' ASMSyntaxArray D§ASMSyntaxArrayLen
Initialize ASMSyntaxArray to zero.

                call 'KERNEL32.GetModuleFileNameA' D§hInstance HiliteBuffer 1024
        call 'KERNEL32.lstrlen' HiliteBuffer
        mov ecx eax | dec ecx
        mov edi HiliteBuffer | add edi ecx
        std
            mov al '\'
            repne scasb
        cld
        inc edi | mov B§edi 0
        call 'KERNEL32.lstrcat' HiliteBuffer WordFileName

Construct the full path name of wordfile.txt: I assume that it's always in the same folder as the program.

        call 'KERNEL32.GetFileAttributesA' HiliteBuffer
        ...If eax <> 0-1
I use this method as a quick way of checking whether a file exists.
            mov D@BlockSize 10240
            call 'KERNEL32.HeapAlloc' D§hMainHeap 0 D@BlockSize
            mov D@pTemp,eax
Allocate the memory block to store the words. Default to 10K. The memory is allocated from the default heap.
    call 'KERNEL32.GetPrivateProfileStringA' ASMSection #1 ZeroString,
                                             D@pTemp D@BlockSize HiliteBuffer
    .If eax <> 0
I use GetPrivateProfileString to retrieve the content of each key in wordfile.txt. The key starts from C1 to C10.
        inc eax
        If eax = D§FillHiliteInfo@BlockSize         ; the buffer is too small
            add D§FillHiliteInfo@BlockSize 10240
            call 'KERNEL32.HeapReAlloc' D§hMainHeap 0 D§FillHiliteInfo@pTemp D§FillHiliteInfo@BlockSize
            mov D§FillHiliteInfo@pTemp eax
            jmp L1<
         End_If
         mov edx ASMColorArray | add edx #2
         call ParseBuffer D§hMainHeap D§FillHiliteInfo@pTemp eax edx ASMSyntaxArray
    .endif
Checking whether the memory block is large enough. If it is not, we increment the size by 10K until the block is large enough.
         mov edx ASMColorArray | add edx #2
         call ParseBuffer D§hMainHeap D§FillHiliteInfo@pTemp eax edx ASMSyntaxArray
Pass the words, the memory block handle, the size of the data read from wordfile.txt, the address of the color dword that will be used to hilight the words and the address of ASMSyntaxArray.

Now, let's examine what ParseBuffer does. In essence, this function accepts the buffer containing the words to be hilighted ,parses them to individual words and stores each of them in a WORDINFO structure array that can be accessed quickly from ASMSyntaxArray.

Proc ParseBuffer:
    Arguments @hHeap, @pBuffer, @nSize, @ArrayOffset, @pArray
    Local @InProgress
    Uses edi esi
    
        mov D@InProgress &FALSE
InProgress is the flag I use to indicate whether the scanning process has begun. If the value is FALSE, we haven't encountered a non-white space character yet.

                mov esi ParsBuffer, edi D@pBuffer
               call 'USER32.CharLowerA' edi

esi points to our local buffer that will contain the word we have parsed from the word list. edi points to the word list string. To simplify the search later, we convert all characters to lowercase.
        mov ecx D@nSize | or ecx ecx | jz L9>>
        
L0:     cmp B§edi ' ' | je L2>
        cmp B§edi 9 | je L2>                ; tab

Scan the whole word list in the buffer,
looking for the white spaces. If a white space is found, we have to determine
whether it marks the end or the beginning of a word.
            mov D@InProgress &TRUE
            mov al B§edi, B§esi al | inc esi
L1:         inc edi | dec ecx | jz L9>>
                jmp L0<
If the byte under scrutiny is not a white space, we copy it to the buffer to construct a word and then continue the scan.
L2:     cmp D@InProgress &TRUE | jne L1<
If a white space is found, we check the value in InProgress. If the value is TRUE, we can assume that the white space marks the end of a word and we may proceed to put the word currently in the local buffer (pointed to by esi) into a WORDINFO structure. If the value is FALSE, we continue the scan until a non-white space character is found.
L9:     mov B§esi 0                         ; Found
        push ecx

            call 'KERNEL32.HeapAlloc' D@hHeap &HEAP_ZERO_MEMORY WordInfoLen
When the end of a word is found, we append 0 to the buffer to make the word an ASCIIZ string. We then allocate a block of memory from the heap the size of WORDINFO for this word.
            push esi
                mov esi eax
                call 'KERNEL32.lstrlen' ParsBuffer
                mov D§esi+WORDINFO.WordLen eax
We obtain the length of the word in the local buffer and store it in the WordLen member of the WORDINFO structure, to be used as a quick comparison.
                move D§esi+WORDINFO.pColor D@ArrayOffset
Store the address of the dword that contains the color to be used to hilight the word in pColor member.
                inc eax
                call 'KERNEL32.HeapAlloc' D@hHeap &HEAP_ZERO_MEMORY eax
                mov D§esi+WORDINFO.pszWord eax, edx eax
                call 'KERNEL32.lstrcpy' edx ParsBuffer
Allocate memory from the heap to store the word itself. Right now, the WORDINFO structure is ready to be inserted into the appropriate linked list.
                mov eax D@pArray
                movzx edx B§ParsBuffer | shl edx 2  ; multiply by 4
                add eax edx
pArray contains the address of ASMSyntaxArray. We want to move to the dword that has the same index as the value of the first character of the word. So we put the first character of the word in edx then multiply edx by 4 (because each element in ASMSyntaxArray is 4 bytes in size) and then add the offset to the address of ASMSyntaxArray. We have the address of the corresponding dword in eax.
                If D§eax = 0
                    mov D§eax esi
                Else
                    move D§esi+WORDINFO.NextLink D§eax
                    mov D§eax esi
                End_If
Check the value of the dword. If it's 0, it means there is currently no word that begins with this character in the list. We thus put the address of the current WORDINFO structure in that dword.

If the value in the dword is not 0, it means there is at least one word that begins with this character in the array. We thus insert this WORDINFO structure to the head of the linked list and update its NextLink member to point to the next WORDINFO structure.

            pop esi
        pop ecx
        mov esi ParsBuffer, D@InProgress &FALSE
        cmp ecx 0 | ja L1<<
After the operation is complete, we begin the next scan cycle until the end of buffer is reached.
        call 'USER32.SendMessageA' D§hwndRichEdit &EM_SETTYPOGRAPHYOPTIONS,
                                   &TO_SIMPLELINEBREAK &TO_SIMPLELINEBREAK
        call 'USER32.SendMessageA' D§hwndRichEdit &EM_GETTYPOGRAPHYOPTIONS 1 1
        If eax = 0  ; means this message is not processed
            mov D§RichEditVersion 2
        Else
            mov D§RichEditVersion 3
   ;=============================================================================
   ; Make it emulate system edit control so the text color update doesn't  take very long
   ;=============================================================================
            call 'USER32.SendMessageA' D§hwndRichEdit &EM_SETEDITSTYLE,
                                       &SES_EMULATESYSEDIT &SES_EMULATESYSEDIT
        End_If
After the richedit control is created, we need to determine the its version. This step is necessary since EM_POSFROMCHAR behaves differently for RichEdit 2.0 and 3.0 and EM_POSFROMCHAR is crucial to our syntax hilighting routine. I have never seen a documented way of checking the version of richedit control thus I have to use a workaround. In this case, I set an option that is specific to version 3.0 and immediately retrieve its value. If I can retrieve the value, I assume that the control version is 3.0.

If you use RichEdit control version 3.0, you will notice that updating the font color for a large file takes quite a long time. This problem seems to be specific to version 3.0. I found a workaround: making the control emulate the behavior of the system edit control by sending EM_SETEDITSTYLE message.

After we can obtain the version information, we proceed to subclass the richedit control. We will now examine the new window procedure for the richedit control.

Proc NewRichEditProc:
    Arguments @hWnd, @uMsg, @wParam, @lParam
    Local @hdc, @hOldFont, @FirstChar, @hRgn, @hOldRgn, @pString, @BufferSize

    ...If D@uMsg = &WM_PAINT
        push edi
        push esi
        call 'USER32.HideCaret' D@hWnd
        call 'USER32.CallWindowProcA' D§OldWndProc D@hWnd D@uMsg D@wParam D@lParam
        push eax
We handle WM_PAINT message. First, we hide the caret so as to avoid some ugly gfx after the hilighting. After that we pass the message to the original richedit procedure to let it update the window. When CallWindowProc returns, the text is updated with its usual color/background. Now is our opportunity to do syntax hilighting.
        mov edi ASMSyntaxArray
        call 'USER32.GetDC' D@hWnd | mov D@hdc eax
        call 'GDI32.SetBkMode' D@hdc &TRANSPARENT
Store the address of ASMSyntaxArray in edi. Then we obtain the handle to the device context and set the text background mode to transparent so the text that we will write will use the default background color.
        call 'USER32.SendMessageA' D@hWnd &EM_GETRECT 0 VirtRECT
        call 'USER32.SendMessageA' D@hWnd &EM_CHARFROMPOS 0 VirtRECT
        call 'USER32.SendMessageA' D@hWnd &EM_LINEFROMCHAR eax 0
        call 'USER32.SendMessageA' D@hWnd &EM_LINEINDEX eax 0
We want to obtain the visible text so we first have to obtain the formatting rectangle by sending EM_GETRECT message to the richedit control. Now that we have the bounding rectangle, we obtain the nearest character index to the upper left corner of the rectangle with EM_CHARFROMPOS. Once we have the character index (the first visible character in the control), we can start to do syntax hilighting starting from that position. But the effect might not be as good as when we start from the first character of the line that the character is in. That's why I need to obtain the line number of that the first visible character is in by sending EM_LINEFROMCHAR message. To obtain the first character of that line, I send EM_LINEINDEX message.
        mov D§txtrange@cpMin eax
        mov D@FirstChar eax
        call 'USER32.SendMessageA' D@hWnd &EM_CHARFROMPOS 0 VirtRECT@right
        mov D§txtrange@cpMax eax
Once we have the first character index, store it for future reference in FirstChar variable. Next we obtain the last visible character index by sending EM_CHARFROMPOS, passing the lower-right corner of the formatting rectangle in lParam.
        move D§RealRect@left D§VirtRECT@left
        move D§RealRect@top D§VirtRECT@top
        move D§RealRect@right D§VirtRECT@right
        move D§RealRect@bottom D§VirtRECT@bottom
        call 'GDI32.CreateRectRgn' D§RealRect@left D§RealRect@top D§RealRect@right,
                                    D§RealRect@bottom
        mov D@hRgn eax
        call 'GDI32.SelectObject' D@hdc D@hRgn
        mov D@hOldRgn eax
While doing syntax hilighting, I noticed an unsightly side-effect of this method: if the richedit control has a margin (you can specify margin by sending EM_SETMARGINS message to the richedit control), DrawText writes over the margin. Thus I need to create a clipping region, the size of the formatting rectangle, by calling CreateRectRgn. The output of GDI functions will be clipped to the "writable" area.

Next, we need to hilight the comments first and get them out of our way. My method is to search for ";" and hilight the text with the comment color until the carriage return is found. I will not analyze the routine here: it's fairly long and complicated. Suffice here to say that, when all the comments are hilighted, we replace them with 0s in the buffer so that the words in the comments will not be processed/hilighted later.

            mov ecx D@BufferSize
            mov esi NewBuffer
            .While ecx > 0
                mov al B§esi
                On al = ' ', jmp L2>
                On al = 0D, jmp L2>
                On al = '/', jmp L2>
                On al = ',', jmp L2>
                On al = '|', jmp L2>
                On al = '+', jmp L2>
                On al = '-', jmp L2>
                On al = '*', jmp L2>
                On al = '&', jmp L2>
                On al = '<', jmp L2>
                On al = '>', jmp L2>
                On al = '=', jmp L2>
                On al = '(', jmp L2>
                On al = ')', jmp L2>
                On al = '{', jmp L2>
                On al = '}', jmp L2>
                On al = '[', jmp L2>
                On al = ']', jmp L2>
                On al = '^', jmp L2>
                On al = ':', jmp L2>
                On al = 9, jmp L2>
                jmp L3>
L2:             mov B§esi 0
L3:             dec ecx
                inc esi
            .End_While
Once the comments are out of our way, we separate the words in the buffer by replacing the "separator" characters with 0s. With this method, we need not concern about the separator characters while processing the words in the buffer anymore: there is only one separator character, NULL.
            mov esi NewBuffer
            mov ecx D@BufferSize
            ..While ecx > 0
                mov al B§esi
                .If al <> 0
Search the buffer for the first character that is not null,ie, the first character of a word.
                    push ecx
                    call 'KERNEL32.lstrlen' esi
                    push eax
                        mov edx eax  ; edx contains the length of the string
Obtain the length of the word and put it in edx
                        movzx eax B§esi
                        On al < 'A', jmp L2>
                        On al > 'Z', jmp L2>
                            add al 020
L2:
Convert the character to lowercase (if it's an uppercase character)
                        shl eax 2
                        add eax edi  ; edi contains the pointer to the WORDINFO pointer array
                        On D§eax = 0, jmp L7>>
After that, we skip to the corresponding dword in ASMSyntaxArray and check whether the value in that dword is 0. If it is, we can skip to the next word.
                            mov eax D§eax
                       ; assume eax:ptr WORDINFO
                            .While eax <> 0
                                On edx <> D§eax+WORDINFO.WordLen, jmp L6>>
If the value in the dword is non-zero, it points to the linked list of WORDINFO structures. We process to walk the linked list, comparing the length of the word in our local buffer with the length of the word in the WORDINFO structure. This is a quick test before we compare the words. Should save some clock cycles.
                                    pushad
                                        call 'KERNEL32.lstrcmpi' D§eax+WORDINFO.pszWord esi
                                        On eax <> 0, jmp L5>>
If the lengths of both words are equal, we proceed to compare them with lstrcmpi.
                                            popad
                                            mov ecx esi
                                            mov edx NewBuffer
                                            sub ecx edx
                                            add ecx D@FirstChar
We construct the character index from the address of the first character of the matching word in the buffer. We first obtain its relative offset from the starting address of the buffer then add the character index of the first visible character to it.
                                            pushad
                                                If D§RichEditVersion = 3
                                                    call 'USER32.SendMessageA' D@hWnd &EM_POSFROMCHAR,
                                                                            VirtRECT ecx
                                                Else
                                                    call 'USER32.SendMessageA' D@hWnd &EM_POSFROMCHAR,
                                                                            ecx,0
                                                    mov ecx eax | and ecx 0FFFF | mov D§VirtRECT@left ecx
                                                    shr eax 16 | mov D§VirtRECT@top eax
                                                End_If
                                            popad
Once we know the character index of the first character of the word to be hilighted, we proceed to obtain the coordinate of it by sending EM_POSFROMCHAR message. However, this message is interpreted differently by richedit 2.0 and 3.0. For richedit 2.0, wParam contains the character index and lParam is not used. It returns the coordinate in eax. For richedit 3.0, wParam is the pointer to a POINT structure that will be filled with the coordinate and lParam contains the character index.

As you can see, passing the wrong arguments to EM_POSFROMCHAR can wreak havoc to your system. That's why I have to differentiate between RichEdit control versions.

                                            mov edx D§eax+WORDINFO.pColor
                                            call 'GDI32.SetTextColor' D@hdc D§edx
                                            call 'USER32.DrawTextA' D@hdc esi 0-1 VirtRECT 0
Once we got the coordinate to start, we set the text color with the one specified in the WORDINFO structure. And then proceed to overwrite the word with the new color.

As the final words, this method can be improved in several ways. For example, I obtain all the text starting from the first to the last visible line. If the lines are very long, the performance may hurt by processing the words that are not visible. You can optimize this by obtaining the really visible text line by line. Also the searching algorithm can be improved by using a more efficient method. Don't take me wrong: the syntax hilighting method used in this example is FAST but it can be FASTER. :)


[Iczelion's Win32 Assembly Homepage]