SourceView GTK 4 minimal example

Can anyone provide a minimal example of GTK 4 Source View 5 with auto-completion of words found in a document, change syntax highlight lang and change color scheme in any high-level language.

I found one well commented example but its from 2004 https://eurion.net/python-snippets/snippet/GtkSourceView%20Example.html

Link for tutorial on GNOME wiki also 2007 http://www.bravegnu.org/gtktext/. I think there a lot of changes since then.

I wanna create simple example on Nim but I havent any experience with SourceView (especially 5)
It seems that it is very lacking tutorials with examples and not dry API documentation.
Here’s where I left off using gintro (nimble install gintro@#head)

# nimble install gintro@#head
# nim c -r source_view5.nim
import gintro/[gtk4, gio, gobject, gtksource5]
import std/with

proc hello(button: StyleSchemeChooserButton) =
  echo "hello world"

proc activate(app: gtk4.Application) =
  let
    window = newApplicationWindow(app)
    scrolled = newScrolledWindow()
    headerBar = newHeaderBar()
    srcView = newView()
    srcCompleteonSnippets = newCompletionSnippets()
    srcCompleteonWords = newCompletionWords()
    languageManager = getDefaultLanguageManager()
    themeChooser = newStyleSchemeChooserButton()

  
  srcView.completion.addProvider(srcCompleteonSnippets)
  srcView.completion.addProvider(srcCompleteonWords)
    
  
  with srcView:
    autoIndent = true
    acceptsTab = true
    showLineNumbers = true
    showLineMarks = true
    showRightMargin = true
    smartBackspace = true
    smartHomeEnd = SmartHomeEndType.always
    highlightCurrentLine = true
    indentOnTab = true
    indentWidth = 2
    wrapMode = WrapMode.word
    enableSnippets = true

  themeChooser.connect("clicked", hello)
  with themeChooser:
    label = "change Color Scheme"
    connect("clicked", hello)

  with headerBar:
    packStart themeChooser

  scrolled.child = srcView
  with window:
    title = "headerBar"
    setTitlebar headerBar
    setChild scrolled
    show


proc main =
  let app = newApplication("org.gtk.example")
  app.connect("activate", activate)
  discard run(app)

main()

Well, I guess here are not that many Nim and GTK-Sourceview programmers.

In Februar I started porting my old NEd from

This is what I already have: Syntax highlighting and color scheme works with the old NEd files, and Nimsuggest error check too. Not yet completion:

# nim c --gc:arc ned.nim
import gintro/[gtk4, gdk4, gobject, glib, gio, gtksource5, pango]

from OS import paramCount, paramStr, extractFilename, findExe, splitFile, parentDir, removeFile
import osproc
import strutils
import net

const
  NSPort = Port(6000)
  MaxErrorTags = 16
  ErrorTagName = "error"

const menuData = """
  <interface>
    <menu id="menuModel">
      <section>
        <item>
          <attribute name="label">Save As...</attribute>
          <attribute name="action">win.safe-as</attribute>
        </item>
        <item>
          <attribute name="label">check</attribute>
          <attribute name="action">win.check</attribute>
        </item>
        <submenu>
          <attribute name="label">Submenu</attribute>
          <item>
            <attribute name="label">Submenu Item</attribute>
            <attribute name="action">win.submenu-item</attribute>
          </item>
        </submenu>
        <item>
          <attribute name="label">Toggle Menu Item</attribute>
          <attribute name="action">win.toggle-menu-item</attribute>
        </item>
      </section>
      <section>
        <item>
          <attribute name="label">Radio 1</attribute>
          <attribute name="action">win.radio</attribute>
          <attribute name="target">1</attribute>
        </item>
        <item>
          <attribute name="label">Radio 2</attribute>
          <attribute name="action">win.radio</attribute>
          <attribute name="target">2</attribute>
        </item>
        <item>
          <attribute name="label">Radio 3</attribute>
          <attribute name="action">win.radio</attribute>
          <attribute name="target">3</attribute>
        </item>
      </section>
    </menu>
  </interface>"""

# do cleanup work, closing files or saving documents
proc shutdown(app: Application) =
  echo "shutdown"



proc handleLocalOptions(app: Application; vd: VariantDict): int =
  echo "handle-local-options"

proc nameLost(app: Application): bool =
  echo "name-lost"

type
  NimViewError = tuple
    gs: string
    line, col, id: int

type
  NedWindow = ref object of gtk4.ApplicationWindow
    filePath: string
    gFile: GFile
    buffer: gtksource5.Buffer

  NimView = ref object of gtksource5.View
    errors: seq[NimViewError]

proc setErrorAttr(view: NimView) =
  var attrs = newMarkAttributes()
  var color = RGBA(red: 0.5, green: 0.5, blue: 0.5, alpha: 0.3)
  attrs.background = color
  attrs.iconName = "list-remove"
  view.setMarkAttributes(ErrorTagName, attrs, priority = 1)

var thread: system.Thread[NedWindow]
#var channel: system.Channel[StatusMsg]
var nsProcess: Process # nimsuggest

# https://developer.gnome.org/gtk4/unstable/GtkFileChooser.html#gtk-file-chooser-set-file
proc prepareFileChooser(chooser: FileChooserDialog; existingFile: GFile) =
  let documentIsNew = (existingFile == nil)
  if documentIsNew:
      let defaultFileForSaving = newGFileForPath ("./out.txt")
      # the user just created a new document
      discard chooser.setCurrentFolder(defaultFileForSaving)
      chooser.setCurrentName("Untitled document")
  else:
      # the user edited an existing document
      discard chooser.setFile(existing_file)
    
proc fileChooserResponseCb(d: FileChooserDialog; id: int; w: NedWindow) =
  if ResponseType(id) == ResponseType.accept:
    let file = d.file
    echo file.getPath
    w.gFile = file
  d.destroy

proc saveAsCb(action: gio.SimpleAction; parameter: glib.Variant; w: NedWindow) =
  echo("Save As")
  let dialog = newFileChooserDialog("Save File", w, FileChooserAction.save)
  prepareFileChooser(FileChooserDialog(dialog), w.gfile)
  discard dialog.addButton("Save", ResponseType.accept.ord)
  discard dialog.addButton("Cancel", ResponseType.cancel.ord)
  dialog.connect("response", fileChooserResponseCb, w)
  dialog.show

proc saveFile(b: Button; w: NedWindow) =
  let buffer = w.buffer
  let startIter = buffer.getStartIter
  let endIter = buffer.getEndIter
  let text = buffer.getText(startIter, endIter, includeHiddenChars = true)
  let gfile: GFile = newGFileForPath(w.filePath) # never fails
  let res = gfile.replaceContents(text, etag = nil, makeBackup = false, FileCreateFlags({}))
  #  buffer.modified = false
  echo "saveFile"

proc initSuggest(win: NedWindow; path: string) =
  if nsProcess.isNil and path.endsWith(".nim"):
    let file: GFile = newGFileForPath(path)
    if queryExists(file, nil):
      #open(channel)
      let nimBinPath = findExe("nim")
      doAssert(nimBinPath.len > 0, "we need nim executable!")
      let nimsuggestBinPath = findExe("nimsuggest")
      doAssert(nimsuggestBinPath.len > 0, "we need nimsuggest executable!")
      let nimPath = nimBinPath.splitFile.dir.parentDir
      nsProcess = startProcess(nimsuggestBinPath, nimPath,
                         ["--v2", "--threads:on", "--port:" & $NSPort, $path],
                         options = {poStdErrToStdOut, poUsePath})
      #createThread[NedWindow](thread, showData, win)

proc removeMarks(view: NimView) =
  echo "removeMarks"
  let buffer = gtksource5.Buffer(view.buffer)
  #let buffer = view.buffer
  let startIter = buffer.getStartIter
  let endIter = buffer.getEndIter
  buffer.removeTagByName(ErrorTagName, startIter, endIter)
  for i in 0 .. MaxErrorTags:
    buffer.removeTagByName($i, startIter, endIter)
  buffer.removeSourceMarks(startIter, endIter)
  view.showLinemarks = false

## returns dirtypath or "" for failure
proc saveDirty(filepath: string; text: string): string =
  var stream: FileIOStream
  let filename = filepath.splitFile[1] & "XXXXXX.nim"
  let gfile = newGFileTmp(filename, stream)
  if gfile.isNil:
    return
  if gfile.replaceContents(text, etag = nil, makeBackup = false, {FileCreateFlag.private}):
    result = gfile.path

# return errorID > 0 when new error position, or 0 for old position
proc addError(v: NimView, s: string; line, col: int): int =
  echo "addError"
  for el in mitems(v.errors):
    if el.line == line and el.col == col:
      el.gs &= ("\n" & s)
      return 0
  let i = system.int(v.errors.len) + 1
  if i > MaxErrorTags: return 0
  var el: NimViewError
  el.gs = s
  el.line = line
  el.col = col
  el.id = i
  v.errors.add(el)
  return i

proc setErrorMark(view: NimView; ln, cn: int) =
  echo "setErrorMark", ln, " ", cn
  var iter: TextIter
  let buffer = gtksource5.Buffer(view.getBuffer)
  discard buffer.getIterAtLineIndex(iter, ln.cint, cn.cint)
  discard iter.backwardLine
  if ln > 0:
    discard iter.forwardLine
  buffer.removeSourceMarks(iter, iter)
  echo "AAA"
  discard buffer.createSourceMark("", ErrorTagName, iter)
  echo "HA"

proc jumpto(view: NimView; line, column: int) =
  var iter: TextIter
  let buffer = view.buffer
  discard buffer.getIterAtLineIndex(iter, line, column)
  buffer.placeCursor(iter)
  view.scrollToMark(buffer.insert, withinMargin = 0.25, useAlign = false, xalign = 0, yalign = 0)

proc advanceErrorWord(ch: gunichar, userdata: pointer): gboolean {.cdecl.} = gboolean(not unicharIsalnum(ch))

# can not remember why we did it in this way...
proc setErrorTag(view: NimView; ln, cn, id: int) =
  echo ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>setErrorTag"
  var startIter, endIter, iter: TextIter
  let buffer = view.buffer
  discard buffer.getIterAtLineIndex(startIter, ln.cint, cn.cint)
  let tag: TextTag = buffer.tagTable.lookup(ErrorTagName)
  assert(tag != nil)
  discard startiter.backwardChar # separate adjanced error tags
  if startIter.hasTag(tag):
    discard startIter.forwardToTagToggle(tag) # same as forwardChar?
  discard startiter.forwardChar
  endIter = startIter
  iter = startIter
  discard iter.forwardToLineEnd
  discard endIter.forwardChar # check
  discard endIter.forwardFindChar(advanceErrorWord, userData = nil, limit = iter)
  buffer.applyTag(tag, startIter, endIter)
  buffer.applyTagByName($id, startIter, endIter)

proc checkCb(action: gio.SimpleAction; parameter: glib.Variant; app: gtk4.Application) =
  if nsProcess.isNil: return
  var ln, cn: int
  var nerrors, nwarnings: int
  let win = NedWindow(getActiveWindow(app))
  #let win = (getActiveWindow(app))
  let h = ScrolledWindow(win.getChild)
  let view = NimView(h.getChild)
  let buffer = view.getbuffer
  if not win.filePath.endsWith(".nim"):
    #showmsg(win, "File is still unsaved or has no .nim suffix -- action ignored.")
    return
  let startIter = buffer.getStartIter
  let endIter = buffer.getEndIter
  removeMarks(view)
  view.errors.setLen(0)
  let text = buffer.getText(startIter, endIter, includeHiddenChars = true)
  let dirtypath = saveDirty(win.filepath, text)
  if dirtyPath.len == 0: return
  var line = newStringOfCap(240)
  let socket = newSocket()
  socket.connect("localhost", NSPort)
  socket.send("chk " & win.filePath & ";" & dirtypath & ":1:1\c\L")
  var last: string
  var com, sk, sym, sig, path, lin, col, doc, percent: string
  while true:
    var isError: bool
    socket.readLine(line)
    if line.len == 0:
      break
    if line == "\c\l":
      echo "\c\l"
      continue
    if line == last:
      # echo "line == last" # occurs!
      continue
    #if line == "\c\l" or line == last: continue
    (com, sk, sym, sig, path, lin, col, doc, percent) = line.split('\t') # sym is empty
    #echo com, sk, sym, sig, path, lin, col, doc, percent
    if path != win.filePath: continue
    if doc[0] == '"':
      # echo "quoted doc" # occurs
      doc = doc[1 .. ^2]
    # echo "find \\'", doc.find("\\'") # occurs
    doc = doc.replace("\\'", "'")
    # echo "find \\x0A", doc.find("\\x0A") # occurs
    doc = doc.replace("\\x0A", "\n")
    #log(win, line, LogLevel.debug)
    var show: bool
    if sig == "Error":
      isError = true
      show = true
    else:
      if  nwarnings > MaxErrorTags div 2: continue
      isError = false
      show = sig == "Hint" or sig == "Warning"
    if show:
      last = line
      cn = col.parseInt
      ln = lin.parseInt
      if cn < 0 or ln <= 0:
        echo "cn < 0 or ln < 0" # should really not occur
        continue
      ln -= 1
      var id = view.addError(doc, ln, cn)
      if id > 0:
        if isError:
          inc(nerrors)
          setErrorMark(view, ln, cn)
          if nerrors == 1:
            #buffer.signalHandlerBlock(buffer.handlerID) # without showmsg() is overwritten
            jumpto(view, ln, cn)
            discard
            #buffer.signalHandlerUnblock(buffer.handlerID)
        else:
          inc(nwarnings)
        setErrorTag(view, ln, cn, id)
  socket.close
  view.setShowLinemarks(nerrors > 0)
  dirtypath.removeFile
  #showmsg(win, "Errors: " & $nerrors & ", Hints/Warnings: " & $nwarnings)

proc showErrorTooltip(view: NimView; x, y: int; keyboardMode: bool; tooltip: Tooltip): bool =
  var bx, by, trailing: int
  var iter: TextIter
  if keyboardMode: return false
  view.windowToBufferCoords(TextWindowType.widget, x, y, bx, by)
  let table: TextTagTable = view.buffer.tagTable
  var tag: TextTag = table.lookup(ErrorTagName)
  assert(tag != nil)
  discard view.getIterAtPosition(iter, trailing, bx, by)
  if iter.hasTag(tag):
    for e in view.errors:
      tag = table.lookup($e.id)
      if tag != nil:
        if iter.hasTag(tag):
          tooltip.text = e.gs
          return true
  return false

proc toIntVal(i: int): Value =
  let gtype = typeFromName("gint")
  discard init(result, gtype)
  setInt(result, i)


# create the GUI. Currently only one single text window
proc activateOrOpen(app: Application) =
  let window: NedWindow = newApplicationWindow(NedWindow, app)
  window.title = "Plain GTK4 Nim Editor"
  window.defaultSize = (800, 600)
  let scrolledWindow = newScrolledWindow()
  let buffer = newBuffer()
  let view: NimView = newViewWithBuffer(NimView, buffer)
  setErrorAttr(view)
  let tt = newTextTag(ErrorTagName)
  tt.setProperty("underline", toIntVal(pango.Underline.error.ord))
  discard add(buffer.getTagTable, tt)
  for i in 0 .. MaxErrorTags:
    discard add(buffer.getTagTable, newTextTag($i))
  let menubutton = newMenuButton()
  let actionGroup: gio.SimpleActionGroup = newSimpleActionGroup()
  var action: SimpleAction
  action = newSimpleAction("safe-as")
  discard action.connect("activate", saveAsCb, window)
  actionGroup.addAction(action)
  action = newSimpleAction("check")
  action.connect("activate", checkCb, app)
  setAccelsForAction(app, "win.check", "<Control>E")
  actionGroup.addAction(action)
  connect(view, "query-tooltip", showErrorTooltip)
  let header = newHeaderBar() 
  window.setTitlebar(header)
  let fileOpenButton = newButton("Open")
  let fileSaveButton = newButton("Save")
  fileSaveButton.connect("clicked", saveFile, window)
  header.packStart(fileOpenButton)
  header.packEnd(fileSaveButton)
  window.insertActionGroup("win", actionGroup)
  var builder = newBuilderFromString(menuData)
  var menuModel: gio.MenuModel = builder.getMenuModel("menuModel")
  var menu = newPopoverMenuFromModel(menuModel)
  menuButton.setPopover(menu)
  menuButton.setIconName("open-menu-symbolic") 
  header.packEnd(menuButton) 
  let cssProvider = newCssProvider()
  let data = "textview {font-size: 16pt;}"
  cssProvider.loadFromData(data)
  let styleContext = view.getStyleContext
  assert styleContext != nil
  addProvider(styleContext, cssProvider, STYLE_PROVIDER_PRIORITY_USER)
  window.setChild(scrolledWindow)
  scrolledWindow.setChild(view)
  show(window)

# set up and initialize the application
proc startup(app: Application) =
  echo "startup"
  activateOrOpen(app)

# program launch without file arguments, so open a default initial window
proc activate(app: Application) =
  let window: NedWindow = NedWindow(app.getActiveWindow)
  assert(window != nil)
  let h = ScrolledWindow(window.getChild)
  let view: NimView = NimView(h.getChild)
  let buffer: Buffer = Buffer(view.getBuffer)
  window.setTitle("New Document")
  window.buffer = buffer
  window.filePath = ""

# launch with file arguments, display file content
proc open(app: Application; files: seq[GFile]; hint: string) =
  var
    contents: string
    etagOut: string
    length: uint64

  let window: NedWindow = NedWindow(app.getActiveWindow)
  assert(window != nil)
  let h = ScrolledWindow(window.getChild)
  let view: NimView = NimView(h.getChild)
  let buffer: Buffer = Buffer(view.getBuffer)
  if files.len > 0:
    window.gfile = files[0]
    if loadContents(files[0], cancellable = nil, contents, length, etagOut):
      assert length.int == contents.len
      var langManager = getDefaultLanguageManager()
      var styleManager = getDefaultStyleSchemeManager()
      # stylemanager.appendSearchPath("/home/salewski/gtksourceview/data/styles")
      var scheme = stylemanager.getScheme("nimdark1")
      # var langPath = langManager.searchPath
      # langPath.add("/home/salewski/gtksourceview/data/language-specs/")
      # langManager.setSearchPath(langPath)
      var lang = guessLanguage(langManager, files[0].path, nil)
      window.setTitle(files[0].path.extractFilename)
      window.buffer = buffer
      window.filePath = files[0].path
      setLanguage(buffer, lang)
      buffer.setHighlightSyntax
      buffer.setStyleScheme(scheme)
      buffer.setText(contents, contents.len)
      initSuggest(window, window.filePath)

proc commandLine(app: Application; cl: ApplicationCommandLine): int =
  echo "command-line"

# TextCharPredicate* = proc (ch: gunichar; userData: pointer): gboolean {.cdecl.}

proc main =
  let app = newApplication("org.gtk.example", {ApplicationFlag.handlesOpen})#, handlesCommandLine})
  app.connect("startup", startup)
  app.connect("activate", activate)
  app.connect("command-line", commandLine)
  # app.connect("handle_local_options", handleLocalOptions)
  app.connect("open", open)
  app.connect("name-lost", nameLost)
  app.connect("shutdown", shutdown)
  let argLen = paramCount() + 1
  var argStr = newSeq[string](argLen)
  for i in 0 ..< argLen:
    argStr[i] = paramStr(i)
  discard run(app, argLen, argStr) # we have to pass an argString to support open signal handling files setProperty setErrorTag tag add

main()
# 450 lines

I have to admit that gtksourceview is not easy, I may continue in winter.

1 Like

I’m asking for examples of using sourceView with GTK 4 in any languages other than C.
PS @StefanSalewski In the example above, nothing works for me, saving or opening files, nimsuggest or auto-completion of words encountered in the code. Now I will understand why.

By old NEd files you mean nimdark1.xml?

nothing works for me,

You have to compile with threads:on. And nimsuggest should be installed too. nimdark1.xml was my used color file, and nim-lang the language file, stored on my box at

tree .local/share/gtksourceview-5/
.local/share/gtksourceview-5/
β”œβ”€β”€ language-specs
β”‚ └── nim.lang
└── styles
└── nimdark1.xml

But other path should work too. I first copied them where the other GTK4 files lived, but some weeks later they where overwritten by a new GTK4 install.

You may even try the old Aporia GTK2 files, they may work when Aporia is still available.

I think I will continue with my code end of the year, maybe you can wait til them? I would really not enjoy working on it during the summer. Should be at least 100 hours, and I doubt someone other than me would use it or would even be interested in the code. Working on the GTK4 book and other projects have higher priority currently.

[EDIT]

As I installed GTK4 from git sources to /opt I have to do in terminal:

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/gtk/lib64/
GSETTINGS_SCHEMA_DIR=/opt/gtk/share/glib-2.0/schemas ./ned ned.nim

Then SaveAs from menu should open the Save Dialog box. I just noticed that I get a

Gtk-CRITICAL **: 08:50:44.887: gtk_file_chooser_set_file: assertion β€˜G_IS_FILE (file)’ failed

I think this was not there in februar. I really hope that Gentoo Linux ships soon with GTK4, then all these problems should be gone.

It’s not minimal, but I used Source View in python Gtk3 here and here.

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.