My code in Emacs looks like this when using the DRACULA color theme:
I wonder if and how I can also color the class names like SinOsc, SynthDef, etc.
My code in Emacs looks like this when using the DRACULA color theme:
I wonder if and how I can also color the class names like SinOsc, SynthDef, etc.
When I open a scd file without starting the interpreter it actually looks like expected.
However blue are all words starting with capitals. So any word will be formatted in blue.
When I start the interpreter the colors change:
But maybe that’s on purpose?
Seems no one is using Emacs ![]()
I think it may be better to file bugs with the supercollider emacs repository – I believe there are still active maintainers but they might not be following the forum closely.
hjh
I asked here:
Seems to be broken somehow imo.
I fixed it with chatGPT. Have to check if everything is consistent, but it can be easily adjusted.
see here: Syntax Highlighting · Issue #67 · supercollider/scel · GitHub
I had the same problem on Mac M4 (SC 3.14.0), and rolling back from Emacs 30.2 to 28.2 solves the highlighting issue.
/G
Maybe I’m missing something, but sclang-class-tree is not defined anywhere in the current codebase at GitHub - supercollider/scel: Supercollider emacs package
Now I’m puzzled about what is making it work again.
Maybe I found something…
In sclang-mode.el, this line creates a broken regexp:
(let ((regexp (concat "\\<" sclang-class-name-regexp "\\>")))
Since it already starts with \\<, this produces \\<\\<, which is invalid. A possible explanation is that Emacs 28 silently handled this error.
sclang-mode.el around line 314:
(defun sclang-font-lock-class-keyword-matcher (limit)
"Font lock matcher for SuperCollider class names."
(let ((case-fold-search nil))
(catch 'found
(while (re-search-forward sclang-class-name-regexp limit t)
(let ((class-name (match-string-no-properties 0)))
(when (and sclang-class-list
(member class-name sclang-class-list))
(throw 'found t))))
nil)))
Optional: to ensure all classes are available, try this in sclang-language.el
Add (require 'timer) at the top, and this around line 207:
(sclang-set-command-handler
'symbolTable
(lambda (arg)
"Process symbol table from SuperCollider.
Populates class list from ARG. Falls back to direct query if incomplete."
(when (and sclang-use-symbol-table arg)
(setq sclang-symbol-table (sort arg 'string-lessp))
(setq sclang-class-list (cl-remove-if
(lambda (x) (or (not (sclang-class-name-p x))
(sclang-string-match "^Meta_" x)))
sclang-symbol-table))
;; Fallback if incomplete
(when (< (length sclang-class-list) 1000)
(run-at-time 0.5 nil #'sclang-populate-class-list-fully))
(sclang-update-font-lock))))
(defun sclang-populate-class-list-fully ()
"Query SuperCollider directly for all class names.
Fallback when symbol table provides incomplete list (< 1000 classes)."
(when (and (sclang-get-process)
(< (length sclang-class-list) 1000))
(sclang-eval-string
"Class.allClasses.select{|c| c.isMetaClass.not}.collect{|c| c.name.asString}.join($,)"
(lambda (result)
(when result
(setq sclang-class-list (split-string result ","))
(message "Populated %d classes for syntax highlighting" (length sclang-class-list))
(sclang-update-font-lock))))))
To test try M-: (length sclang-class-list)
If other people confirm this works, I can send a patch or open a PR.
I believe this fixes the bug. One question I have is whether someone, perhaps on a less powerful machine, has issues with lag or performance. An improvement in this area would be, as other languages ​​have adopted, using a hash table. However, on a typical laptop, this would only result in a gain of 8ms (reducing it to less than 0.1ms) on a system with 1000-2000 classes. I don’t see this as a problem, but I would like to know other use cases.
Another area for modernization would be replacing Company with Corfu with Cape.
Implementing @scztt’s LSP, using eglot or lsp-mode, still seems uncertain to me due to the issue with UDP sockets. We should wait for a fix with stdin/stdout protocol? Or use a bridge (examples are available in Python and Node.js on GitHub)? I don’t know.
EDIT: On the other hand, sclang already has very good introspection, and Emacs could already use this path to provide a good number of “LSP-like” features (maybe 80-90% of be “basic” LSP features?) without bridges or external language servers. Just food for thought. LSP is really useful in languages like Haskell or Rust, but sclang power already lies in live, dynamic interaction, not large static codebases. That’s why I wonder.
Looks good and classes are now correctly highlighted with your fixes!
(length sclang-class-list) gives 1154. Is this how it is supposed to be?
How does company know which classes are present?
The auto completion doesn´t seem to be complete. I’m getting some suggestions, but e.g. LFNoise1, LFNoise2, etc. or Balance is missing in the auto completion list.
edit: My fault. I added a snippet to prevent number completion. This also affected LFNoise1,2,3 etc.
Glad it is working for you, too!
One change I adopted for myself was to avoid using the mini-buffer for previewing arguments and instead use the editing buffer with a temporary text, eliminating the need for a keybinding. Like that:
Does that appeal to more people?
I think it depends on how good one is with Supercollider. For beginners the inline preview is better I think. Can you post the modification, so I can test for myself and see how it behaves?
@Lilith93 Thanks
After typing (, method arguments appear as an overlay next to your cursor. Pressing , advances the highlight to the next parameter. This builds on SCEL’s existing methodArgs command but renders the information directly in the buffer rather than the minibuffer—eliminating the need for manual keybinding triggers.
( and track your position through argumentsscdoc-modeSCEL already provides runtime introspection, where method arguments are passed directly from the interpreter, along with class/method metadata that we can leverage.
This opens the door for improved IDE features (underexplored, in my opinion):
Synth(\name, ...) or Pbind(\instrument, \name, ...), suggest actual control names and defaults from SynthDescLibHere’s a minimal working example for testing. Note: this replaces SCEL’s methodArgs handler for demonstration purposes—production code keeps the original ( I kept it as simple as possible here):
;;; sclang-inline-signatures.el --- -*- lexical-binding: t; -*-
;; Usage:
;; (require 'sclang-inline-signatures)
;; (add-hook 'sclang-mode-hook #'sclang-inline-signatures-mode)
(require 'cl-lib)
(require 'subr-x)
(require 'sclang-mode)
(require 'sclang-interp)
(defgroup sclang-inline-signatures nil
"Inline signature hints ."
:group 'sclang)
(defcustom sclang-inline-idle-delay 0.10
"Idle debounce (seconds) before showing/updating ."
:type 'number :group 'sclang-inline-signatures)
(defcustom sclang-inline-timeout 2.5
"Seconds before auto-hiding ."
:type 'number :group 'sclang-inline-signatures)
(defface sclang-inline-signature
'((t (:inherit tooltip)))
"Face for the inline container."
:group 'sclang-inline-signatures)
(defface sclang-inline-param-current
'((t (:weight bold :slant italic)))
"Face for the current argument."
:group 'sclang-inline-signatures)
(defface sclang-inline-param-other
'((t (:inherit default)))
"Face for non-current args."
:group 'sclang-inline-signatures)
(defvar-local sclang-inline--ov nil)
(defvar-local sclang-inline--timer nil)
(defvar-local sclang-inline--idle nil)
(defvar-local sclang-inline--last-sig nil)
(defvar-local sclang-inline--param-ix 0)
(defvar sclang-inline--req-buf nil)
(defvar sclang-inline--req-pt nil)
(defun sclang-inline--ready-p ()
(let ((p (sclang-get-process)))
(and p (process-live-p p) (sclang-library-initialized-p))))
(defun sclang-inline--hide ()
(when (overlayp sclang-inline--ov)
(delete-overlay sclang-inline--ov)
(setq sclang-inline--ov nil))
(when (timerp sclang-inline--timer)
(cancel-timer sclang-inline--timer)
(setq sclang-inline--timer nil)))
(defun sclang-inline--schedule-hide ()
(when (timerp sclang-inline--timer) (cancel-timer sclang-inline--timer))
(let ((buf (current-buffer)))
(setq sclang-inline--timer
(run-at-time sclang-inline-timeout nil
(lambda ()
(when (buffer-live-p buf)
(with-current-buffer buf (sclang-inline--hide))))))))
(defun sclang-inline--format (sig ix)
"Format SIG which looks like \"(a, b=1, c)\" and emphasize index IX."
(if (and (stringp sig)
(>= (length sig) 2)
(eq (aref sig 0) ?\()
(eq (aref sig (1- (length sig))) ?\)))
(let* ((params-str (substring sig 1 -1))
(params (if (string-empty-p params-str)
nil
(split-string params-str ",\\s-*" t)))
(pieces (cl-loop for i from 0 for p in params
collect (propertize p 'face
(if (= i ix)
'sclang-inline-param-current
'sclang-inline-param-other)))))
(format "< %s >" (if pieces (string-join pieces ", ") "")))
sig))
(defun sclang-inline--put (str)
(sclang-inline--hide)
(setq sclang-inline--ov (make-overlay (point) (point) nil t t))
(overlay-put sclang-inline--ov 'after-string
(propertize (format " %s " str) 'face 'sclang-inline-signature))
(overlay-put sclang-inline--ov 'priority 1001)
(sclang-inline--schedule-hide))
(defun sclang-inline--show (sig)
(when (and sig (not (string-empty-p sig)))
(sclang-inline--put (sclang-inline--format sig sclang-inline--param-ix))
(setq sclang-inline--last-sig sig)))
(defun sclang-inline--request-args ()
(when (sclang-inline--ready-p)
(setq sclang-inline--req-buf (current-buffer)
sclang-inline--req-pt (point))
(sclang-show-method-args)))
(sclang-set-command-handler
'methodArgs
(lambda (args)
(when (and args sclang-inline--req-buf)
(when (buffer-live-p sclang-inline--req-buf)
(with-current-buffer sclang-inline--req-buf
(goto-char (or sclang-inline--req-pt (point)))
(setq sclang-inline--param-ix 0)
(sclang-inline--show args))))
(setq sclang-inline--req-buf nil
sclang-inline--req-pt nil)))
(defun sclang-inline--inside-string-or-comment-p ()
(let ((s (syntax-ppss))) (or (nth 3 s) (nth 4 s))))
(defun sclang-inline--idle-dispatch ()
(unless (sclang-inline--inside-string-or-comment-p)
(pcase last-command-event
(?\( (setq sclang-inline--param-ix 0)
(sclang-inline--request-args))
(?, (when sclang-inline--last-sig
(cl-incf sclang-inline--param-ix)
(sclang-inline--show sclang-inline--last-sig)))
(?\) (sclang-inline--hide)))))
(defun sclang-inline--arm ()
(when (timerp sclang-inline--idle) (cancel-timer sclang-inline--idle))
(setq sclang-inline--idle
(run-with-idle-timer sclang-inline-idle-delay nil
#'sclang-inline--idle-dispatch)))
;;;###autoload
(define-minor-mode sclang-inline-signatures-mode
"Inline signature overlays for SuperCollider."
:lighter " SC·args"
(if sclang-inline-signatures-mode
(progn
(add-hook 'post-self-insert-hook #'sclang-inline--arm nil t)
(add-hook 'window-configuration-change-hook #'sclang-inline--hide nil t)
(add-hook 'kill-buffer-hook #'sclang-inline--hide nil t))
(remove-hook 'post-self-insert-hook #'sclang-inline--arm t)
(remove-hook 'window-configuration-change-hook #'sclang-inline--hide t)
(remove-hook 'kill-buffer-hook #'sclang-inline--hide t)
(sclang-inline--hide)))
(provide 'sclang-inline-signatures)
;;; sclang-inline-signatures.el ends here
I was seeing the same highlighting issue.
As an emacs/elisp novice I didn’t get far looking at the sclang-mode files.
I decided to try and use Mads Kjeldgaard’s tree-sitter-supercollider grammar with Emacs’ built in treesitter support to get proper highlighting.
The resulting sclang-ts-mode highlighting works well, thanks to some LLM hand holding.
A colour overload, but good for testing:
But my lack of knowledge started to get me stuck when trying to get the indentation to work as expected.
So now I am back to sclang-mode and happy to see a possible highlighting fix!
@smoge your other additions sound exciting also, I was thinking about something like that just some days ago. I’ll check those out as well. Thanks for sharing!
That is excellent news @dgk
I will take a look at tree-sitter. I mentioned it on another thread here, about writing a simple linter for try {}
EDIT: Perhaps you could write some installation instructions for your sclang-ts-mode, if it has any special requirements compared to other modes. It could be included in the README once this is mature (looks pretty good already).
I will clean up the sclang-ts-mode code a bit and put it on github.
Especially the indentation is not fully worked out. It would be great if it could function as a starting point for people with a deeper understanding of these subjects to experiment.
Hey yo @dgk
I would like to express my gratitude to everyone involved for their tireless efforts and dedication. It’s truly inspiring to see our community making remarkable progress together. We’ve traveled a long road, and I’m proud of what we’ve built.
Just one quick note for now: I’ve noticed that the indentation issues are primarily tied to tree-sitter rather than your elisp code.
The role of tree-sitter is largely focused on syntactic recognition, enabling it to identify structures such as binary expressions, function calls, method calls, named arguments, and associative items. It’s noteworthy to note that TS doesn’t delve too deep into semantic meanings.
For instance, in sclang, every binary operator operates as a method selector, and while the symbol / indicates division, %/ signifies a rational operation in my quark, for example.
When the grammar attempts to incorporate semantic elements excessively, it can lead to confusion, and in the case of SC, complexity. This may result in named arguments being misinterpreted as operators, associations encroaching upon other expressions, and precedence being incorrectly established.
For example, you might see .midiratio erroneously applied to the wrong receiver. Your code was:
freq = \freq.kr(440) * (Env.perc(..., curve: -1).ar * 48 * \bend.kr(1)).midiratio;
Parse says:
value: (function_call
(receiver
(binary_expression
left: (function_call ... \freq.kr(440))
right: (code_block (...))))
(method_call name: midiratio))
So .midiratio is a method applied to the whole left * right, not to the parenthesized RHS.
Navigating the intricacies of semantic overlaps can occasionally lead to parsing errors, such as the emergence of curve: alongside its value under name:, which suggests a potential bug in the rule.
You have:
Env.perc(0.001, 0.08, curve: -1)
Parse shows:
(named_argument
name: (identifier curve)
name: (unary_expression -1))
So the value is stored under name: (duplicated) instead of value:.
the grammar defines both sides with field("name", ...).
change rule to:
named_argument: $ => seq(
field('name', choice($.symbol, $.identifier)),
':',
field('value', $._object)
);
SuperCollider has a distinctive precedence system that evaluates all binary operations strictly from left to right, setting it apart from conventional mathematics. So, in SuperCollider, the expression 2 + 3 * 4 actually evaluates as (2 + 3) * 4. In this system, messages take precedence over binary operations (they are kinda considered as an “operator” in the current TS grammar). The parser outlines clear precedence levels: the colon (
has the lowest priority, followed by binary operators and the dot (.), with equals (=) and reference (`) exhibiting right associativity.
Meanwhile, the tree-sitter employs rules for operator precedence. Since many languages do this differently from sclang, it may have been there by mistake from another grammar. However, even those small human mistakes (which can happen by accident) can be brutal when implemented in practical code.
in sc, control structures use function arguments instead of traditional built-in syntax. For example, an if statement is structured as: if(condition, { trueAction }, { falseAction }). Likewise, while loops and for loops introduce iteration: you can write while({ testCondition }, { loopAction }). Additionally, switch and case statements enable flexible conditional branching.
sclang supports a variety of array and collection syntax, with dynamic arrays represented by square brackets, as shown in [1, 2, 3]. Literal arrays prefixed with a hash compile as constants: #[1, 2, 3]. Series notation systematically creates ranges, like (1…10), resulting in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]. Similarly, the arithmetic series (1, 3..11) yields [1, 3, 5, 7, 9, 11], while the replication operator (!) duplicates values, as in 5!3, which gives [5, 5, 5].
Moreover, special operators enhance functionality in domain-specific contexts. The at sign (@) generates Points, like 3@4, while the arrow (->) creates Associations for dictionaries. etc etc usw.
For a deeper check, here is the code and the TS output:
freq = \freq.kr(440) * (Env.perc(..., curve: -1).ar * 48 * \bend.kr(1)).midiratio;
$ npx tree-sitter parse '/home/smoge/scwork/ts/tree-sitter-supercollider/test.scd'
(source_file [0, 0] - [1, 0]
(variable_definition [0, 0] - [0, 89]
name: (variable [0, 0] - [0, 4]
(local_var [0, 0] - [0, 4]
name: (identifier [0, 0] - [0, 4])))
value: (function_call [0, 7] - [0, 89]
(receiver [0, 7] - [0, 79]
(binary_expression [0, 7] - [0, 79]
left: (function_call [0, 7] - [0, 20]
(receiver [0, 7] - [0, 12]
(literal [0, 7] - [0, 12]
(symbol [0, 7] - [0, 12]
(identifier [0, 8] - [0, 12]))))
(method_call [0, 12] - [0, 20]
name: (method_name [0, 13] - [0, 15])
(parameter_call_list [0, 16] - [0, 19]
(argument_calls [0, 16] - [0, 19]
(unnamed_argument [0, 16] - [0, 19]
(literal [0, 16] - [0, 19]
(number [0, 16] - [0, 19]
(integer [0, 16] - [0, 19]))))))))
right: (code_block [0, 23] - [0, 79]
(function_call [0, 24] - [0, 78]
(receiver [0, 24] - [0, 72]
(binary_expression [0, 24] - [0, 72]
left: (binary_expression [0, 24] - [0, 64]
left: (function_call [0, 24] - [0, 59]
(receiver [0, 24] - [0, 27]
(class [0, 24] - [0, 27]))
(method_call [0, 27] - [0, 56]
name: (method_name [0, 28] - [0, 32])
(parameter_call_list [0, 33] - [0, 55]
(argument_calls [0, 33] - [0, 38]
(unnamed_argument [0, 33] - [0, 38]
(literal [0, 33] - [0, 38]
(number [0, 33] - [0, 38]
(float [0, 33] - [0, 38])))))
(argument_calls [0, 40] - [0, 44]
(unnamed_argument [0, 40] - [0, 44]
(literal [0, 40] - [0, 44]
(number [0, 40] - [0, 44]
(float [0, 40] - [0, 44])))))
(argument_calls [0, 46] - [0, 55]
(named_argument [0, 46] - [0, 55]
name: (identifier [0, 46] - [0, 51])
name: (unary_expression [0, 53] - [0, 55]
right: (literal [0, 54] - [0, 55]
(number [0, 54] - [0, 55]
(integer [0, 54] - [0, 55]))))))))
(method_call [0, 56] - [0, 59]
name: (method_name [0, 57] - [0, 59])))
right: (literal [0, 62] - [0, 64]
(number [0, 62] - [0, 64]
(integer [0, 62] - [0, 64]))))
right: (literal [0, 67] - [0, 72]
(symbol [0, 67] - [0, 72]
(identifier [0, 68] - [0, 72])))))
(method_call [0, 72] - [0, 78]
name: (method_name [0, 73] - [0, 75])
(parameter_call_list [0, 76] - [0, 77]
(argument_calls [0, 76] - [0, 77]
(unnamed_argument [0, 76] - [0, 77]
(literal [0, 76] - [0, 77]
(number [0, 76] - [0, 77]
(integer [0, 76] - [0, 77])))))))))))
(method_call [0, 79] - [0, 89]
name: (method_name [0, 80] - [0, 89])))))
Note: keeping all inside TS. Another design would require some thoughtful discussion, I imagine. Some hybrids solutions are possible, and maybe necessary in some more complex cases, like JitLib.
binary_expression: $ => prec.left(PREC.BIN, seq(
field('left', $._object),
field('operator', choice(
'||','&&','|','^','&','==','!=','<','<=','>','>=',
'<<','>>','+','-','++','+/+','*','/','%','**'
)),
field('right', $._object)
));
Left-assoc matches sclang’s evaluation.
const PREC = { CALL: 140, BIN: 20 /* … */ };
function_call: $ => prec.left(PREC.CALL, seq(
field('receiver', $._primary),
repeat1($.method_call)
));
parameter_call_list: $ => sepBy1(',', choice($.named_argument, $._object));
named_argument: $ => seq(
field('name', choice($.symbol, $.identifier)),
':',
field('value', $._object)
);
So, yeah, these grammar fixes will help for now, but honestly, they raise a deeper discussion about what Tree-Sitter should be about.
I will try to write a follow-up post with a proper suggestion for splitting syntax and semantics. Just to make my point clear. The basic idea is to let TS handle the syntax (which it excels at) and build a separate layer that actually understands how sclang works.
Note: SuperCollider’s parser implementation lives in lang/LangSource/Bison/lang11d, using a Yacc/Bison grammar that generates C++ code for AST construction. The parser works with a hand-written lexer in PyrLexer.cpp
Additional layers could do
a) Semantic analysis, for example, track variable scoping across different declaration types, validate primitive operation syntax, and ensure proper resource management patterns. Coding constructs (“small languages” in some sense), such as SynthDefs, Patterns, Ndef, and ProxySpace, require special handling semantics.
b) Linting and “code actions” integration requires detecting the risky practices identified: infinite loops, resource leaks, and blocking operations (just like the try {^ } case). The grammar should enable the identification of common safety checks.
c) …
Peace out
cc: @madskjeldgaard @jamshark70 @julian @scztt @muellmusik @semiquaver
Hey @smoge
I am not sure there is actually need for Tree-sitter code parsing in sclang-mode.
You are touching on points that go beyond my understanding of the matter at hand and beyond.
Aren’t some of you ideas LSP related?
My work is more of a hack to try and get the syntax highlighting working for sc code in sclang-mode and the indentation was necessary because I failed to keep the sclang-mode indentation working in my sclang-ts-mode.
Here is a link to the code on Github: sclang-ts-mode.el
I am interested to read about possible improvements.
Now I will come out of the tree and try your actual fix for the sclang-mode syntax highlighting issue. ![]()