supercollider
supercollider copied to clipboard
added line numbers to code mirror of the help document
Purpose and Motivation
Reading long code examples in help documents is not as straightforward as in SC-IDE's code editor because there are no line numbers on the left side of the code mirror in help documents. This PR adds line numbers to the code mirror of the help document.
https://scsynth.org/t/implementing-line-numbers-on-the-left-side-of-the-code-editor-in-schelp-files/5986
Here are some examples:
Types of changes
- New feature
To-do list
- [x] Code is tested
- [x] All tests are passing
- [x] This PR is ready for review
It would be good if they could be limited to longer portions of code, maybe? They are not helping much in short snippets.
@telephon
Thanks for your comment!
I was unsure how many lines to show the line number from, so I just made all code snippets show their line numbers.
I have provisionally updated it: Currently, line numbers are only shown for code snippets that exceed 10 lines. I would appreciate further clarification on the criteria for displaying line numbers. It is possible that some users may not find it necessary to have line numbers for code snippets that are 15, 20 or 30 lines long.
It is possible that some users may not find it necessary to have line numbers for code snippets that are 15, 20 or 30 lines long.
Yes, this is true. I find line numbers only useful when talking about code and then wanting to point at a line. Otherwise I don't find them all that useful.
An interactive show/hide option would of course be the best in such cases. It could be linked to the default in language preferences.
It could be linked to the default in language preferences.
Unfortunately, my coding skills are not that advanced. What I could do is add a 'show/hide line number' button below each code snippet (default: hidden):
Will it be better if I update this PR with the code below?
const init = () => {
/* based on editors/sc-ide/core/sc_lexer.cpp */
CodeMirror.defineSimpleMode('scd', {
start: [
{ regex: /^\s+/, token: 'whitespace' },
{ regex: /^(?:arg|classvar|const|super|this|var)\b/, token: 'keyword' },
{ regex: /^(?:false|inf|nil|true|thisFunction|thisFunctionDef|thisMethod|thisProcess|thisThread|currentEnvironment|topEnvironment)\b/, token: 'built-in' },
{ regex: /^\b\d+r[0-9a-zA-Z]*(\.[0-9A-Z]*)?/, token: 'number radix-float' },
{ regex: /^\b\d+(s+|b+|[sb]\d+)\b/, token: 'number scale-degree' },
{ regex: /^\b((\d+(\.\d+)?([eE][-+]?\d+)?(pi)?)|pi)\b/, token: 'number float' },
{ regex: /^\b0x(\d|[a-f]|[A-F])+/, token: 'number hex-int' },
{ regex: /^\b[A-Za-z_]\w*\:/, token: 'symbol symbol-arg' },
{ regex: /^[a-z]\w*/, token: 'text name' },
{ regex: /^\b[A-Z]\w*/, token: 'class' },
{ regex: /^\b_\w+/, token: 'primitive' },
{ regex: /^\\\w*/, token: 'symbol' },
{ regex: /'(?:[^\\]|\\.)*?(?:'|$)/, token: 'symbol' },
{ regex: /^\$\\?./, token: 'char' },
{ regex: /^~\w+/, token: 'env-var' },
{ regex: /^\/\/[^\r\n]*/, token: 'comment single-line-comment' },
{ regex: /"(?:[^\\]|\\.)*?(?:"|$)/, token: 'string' },
{ regex: /^[-.,;#()\[\]{}]/, token: 'text punctuation' },
{ regex: /\/\*/, push: 'comment', token: 'comment multi-line-comment' },
{ regex: /^[+\-*/&\|\^%<>=!?]+/, token: 'text operator' },
],
comment: [
{ regex: /\*\//, pop: true, token: 'comment multi-line-comment' },
{ regex: /./, token: 'comment multi-line-comment' }
]
})
let textareas = Array.from(document.querySelectorAll('textarea'))
textareas.forEach(textarea => {
let code = textarea.value
textarea.editor = CodeMirror.fromTextArea(textarea, {
mode: 'scd',
value: code,
lineWrapping: true,
viewportMargin: Infinity,
lineNumbers: false/*code.split('\n').length > 10*/,
extraKeys: {
// noop: prevent both codemirror and the browser to handle Shift-Enter
'Shift-Enter': ()=>{},
// prevent only codemirror to handle Ctrl+D
'Ctrl-D': false
}
})
// Add line number toggle button
let lineNumbersButton = document.createElement('button')
lineNumbersButton.textContent = 'Show/hide Line Numbers'
lineNumbersButton.addEventListener('click', () => {
textarea.editor.setOption('lineNumbers', !textarea.editor.getOption('lineNumbers'))
})
// Append the button below the textarea.editor
textarea.parentNode.appendChild(lineNumbersButton, textarea.nextSibling)
textarea.editor.on('dblclick', editor => {
let cursor = editor.getCursor()
let parenMatch = editor.getLine(cursor.line)
.slice(cursor.ch - 1, cursor.ch).match(/[()]/)
if (parenMatch) {
editor.undoSelection()
selectRegion({ flash: false })
}
})
textarea.editor.on('blur', editor => {
editor.setSelection(editor.getCursor(), null, { scroll: false })
})
textarea.editor.on('dblclick', editor => {
let cursor = editor.getCursor()
let parenMatch = editor.getLine(cursor.line)
.slice(cursor.ch-1,cursor.ch).match(/[()]/)
if (parenMatch) {
editor.undoSelection()
selectRegion({ flash: false })
}
})
textarea.editor.on('blur', editor => {
editor.setSelection(editor.getCursor(), null, { scroll: false })
})
})
}
/* returns the code selection, line or region */
const selectRegion = (options = { flash: true }) => {
let range = window.getSelection().getRangeAt(0)
let textarea = range.startContainer.parentNode.previousSibling
if (!textarea) return
let editor = textarea.editor
if (editor.somethingSelected())
return selectLine(options)
const findLeftParen = cursor => {
let cursorLeft = editor.findPosH(cursor, -1, 'char')
let token = editor.getTokenTypeAt(cursor) || ''
if (cursorLeft.hitSide)
return cursorLeft
let ch = editor.getLine(cursorLeft.line)
.slice(cursorLeft.ch, cursorLeft.ch+1)
if (token.match(/^(comment|string|symbol|char)/))
return findLeftParen(cursorLeft)
if (ch === ')')
return findLeftParen(findLeftParen(cursorLeft))
if (ch === '(')
return cursorLeft
return findLeftParen(cursorLeft)
}
const findRightParen = cursor => {
let cursorRight = editor.findPosH(cursor, 1, 'char')
let token = editor.getTokenTypeAt(cursor) || ''
if (cursorRight.hitSide)
return cursorRight
let ch = editor.getLine(cursorRight.line)
.slice(cursorRight.ch-1, cursorRight.ch)
if (ch === '(')
return findRightParen(findRightParen(cursorRight))
if (ch === ')')
return cursorRight
if (token.match(/^(comment|string|symbol|char)/))
return findRightParen(cursorRight)
return findRightParen(cursorRight)
}
let cursor = editor.getCursor()
if (editor.getLine(cursor.line).slice(cursor.ch,cursor.ch+1) === '(')
editor.setCursor(Object.assign(cursor, { ch: cursor.ch+1 }))
if (editor.getLine(cursor.line).slice(cursor.ch-1,cursor.ch) === ')')
editor.setCursor(Object.assign(cursor, { ch: cursor.ch-1 }))
let parenPairs = []
let leftCursor = findLeftParen(cursor)
let rightCursor = findRightParen(cursor)
while (!leftCursor.hitSide || !rightCursor.hitSide) {
parenPairs.push([leftCursor, rightCursor])
leftCursor = findLeftParen(leftCursor)
rightCursor = findRightParen(rightCursor)
}
/* no parens found */
if (parenPairs.length === 0)
return selectLine(options)
let pair = parenPairs.pop()
leftCursor = pair[0]
rightCursor = pair[1]
/* parens are inline */
if (leftCursor.ch > 0)
return selectLine(options)
/* parens are a region */
if (options.flash === false) {
editor.addSelection(leftCursor, rightCursor)
return editor.getSelection()
} else {
let marker = editor.markText(leftCursor, rightCursor, { className: 'text-flash' })
setTimeout(() => marker.clear(), 300)
return editor.getRange(leftCursor, rightCursor)
}
}
// Returns the code selection or line
const selectLine = (options = { flash: true }) => {
let range = window.getSelection().getRangeAt(0)
let textarea = range.startContainer.parentNode.previousSibling
if (!textarea) return
let editor = textarea.editor
let cursor = editor.getCursor()
if (editor.somethingSelected()) {
from = editor.getCursor('start')
to = editor.getCursor('end')
} else {
from = { line: cursor.line, ch: 0 }
to = { line: cursor.line, ch: editor.getLine(cursor.line).length }
}
if (!options.flash)
return editor.getRange(from, to)
let marker = editor.markText(from, to, { className: 'text-flash' })
setTimeout(() => marker.clear(), 300)
return editor.getRange(from, to)
}
init()
Unfortunately, my coding skills are not that advanced. What I could do is add a 'show/hide line number' button below each code snippet (default: hidden):
hm, that would also be a bit much for each example. Maybe one on the top navigation for all the examples?
@telephon
Maybe one on the top navigation for all the examples?
I have done it! How about this?
- default: showing code line number
- hidden
@telephon Here is another version:
- Instead of a text label, an icon is used.
- After changing the show/hide code line number, the HTML page now shows the previous scrollY.
- The code line numbers are not above the menu bar.
I think this is the maximum I can do. If this is not good, I need help from other contributors.
@telephon I think the image used to indicate the show/hide status of code line numbers is too ugly. I redesigned it. Now I am satisfied:
-
when loading a help document:
-
code line numbers are hidden by clicking the button:
I think showing this button at the top of the page is better than having this option in the IDE preferences, because you can change the status in web browsers too. The only inconvenience is that the user has to click it every time the page is reloaded because the last state of the show/hide code line number is not remembered.
This is really the maximum of my capacity and my best!
I think showing this button at the top of the page is better than having this option in the IDE preferences, because you can change the status in web browsers too.
I think you've found a good compromise, nice work!
(default: hidden):
Can you confirm that the default behavior will still be 'hidden'?
My personal feeling, after seeing the many examples you've shared here and elsewhere is that in general I find the line numbers distracting, but could be nice in certain situations, so my vote would be hidden by default.
The only inconvenience is that the user has to click it every time the page is reloaded because the last state of the show/hide code line number is not remembered.
I'd imagine those using the feature, for example in a classroom setting, would be spending longer periods of time on the page (walking through tutorials, etc.), so enabling it on a page-by-page basis seems fine for now. Linking the setting to IDE preferences could be added later if requested.
@mtmccrea
Can you confirm that the default behavior will still be 'hidden'?
Thanks for your suggestion! I have changed the default behaviour as you suggested!
@capital-G , @mtmccrea , @telephon This PR has been revised to match the gutter background colour of the code line numbers to the background colour of the body in the merged PR #6095.
Conflicts between this PR and the development branch are resolved.
The position of the 'code line number position' is now to the right of the 'theme switcher'.
Some points from my POV regarding the technical implementation
- Instead of setting CSS attributes via JS, you should declare the necessary attributes via a class in CSS and (de-)activate this by de/attaching the class on each relevant HTML element - this keeps CSS code in the CSS file
- you could add a "Show LOC numbers" checkbox within the theme menu - the header is already quite wide and as it is related to theme, it could be fitting there?
- the setting of showing LOC could be stored in
localStoragelike the theme settings, so it would be persistent across sites and reloads - I think the easiest way to toggle this would be to always render the line numbers via a gutter and just hide them via CSS/HTML classes. https://codemirror.net/examples/gutter/ mentions
Unless the cm-mygutter CSS class sets some minimum width, you won't see such a gutter though—it'll just be an empty element (in a CSS flexbox), which the browser will collapse.
If you have some questions I am happy to provide further infos or assist :) I really think this feature would be a good addition! 👍