Skip to content

Commit

Permalink
feat(hog): syntax highlighting for monaco (#23043)
Browse files Browse the repository at this point in the history
  • Loading branch information
mariusandra authored Jun 18, 2024
1 parent 412dc78 commit 5ec6d44
Show file tree
Hide file tree
Showing 8 changed files with 1,153 additions and 27 deletions.
42 changes: 39 additions & 3 deletions frontend/src/lib/components/CodeEditors.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import './CodeEditor.scss'

import MonacoEditor, { type EditorProps } from '@monaco-editor/react'
import MonacoEditor, { type EditorProps, Monaco } from '@monaco-editor/react'
import { useValues } from 'kea'
import { Spinner } from 'lib/lemon-ui/Spinner'
import * as hog from 'lib/monaco/hog'
import * as hogQL from 'lib/monaco/hogql'
import { inStorybookTestRunner } from 'lib/utils'
import { useEffect, useRef, useState } from 'react'
import AutoSizer from 'react-virtualized/dist/es/AutoSizer'
Expand All @@ -11,9 +13,26 @@ import { themeLogic } from '~/layout/navigation-3000/themeLogic'

export type CodeEditorProps = Omit<EditorProps, 'loading' | 'theme'>

export function CodeEditor({ options, ...editorProps }: CodeEditorProps): JSX.Element {
const { isDarkModeOn } = useValues(themeLogic)
function registerHog(monaco: Monaco): void {
if (monaco.languages.getLanguages().some(({ id }) => id === 'hog')) {
return
}
monaco.languages.register({ id: 'hog', extensions: ['.hog'], mimetypes: ['application/hog'] })
monaco.languages.setLanguageConfiguration('hog', hog.conf)
monaco.languages.setMonarchTokensProvider('hog', hog.language)
}

function registerHogQL(monaco: Monaco): void {
if (monaco.languages.getLanguages().some(({ id }) => id === 'hogql')) {
return
}
monaco.languages.register({ id: 'hogql', extensions: ['.sql', '.hogql'], mimetypes: ['application/hog+ql'] })
monaco.languages.setLanguageConfiguration('hogql', hogQL.conf)
monaco.languages.setMonarchTokensProvider('hogql', hogQL.language)
}

export function CodeEditor({ options, onMount, ...editorProps }: CodeEditorProps): JSX.Element {
const { isDarkModeOn } = useValues(themeLogic)
const scrollbarRendering = !inStorybookTestRunner() ? 'auto' : 'hidden'

return (
Expand All @@ -22,6 +41,12 @@ export function CodeEditor({ options, ...editorProps }: CodeEditorProps): JSX.El
loading={<Spinner />}
options={{
// :TRICKY: We need to declare all options here, as omitting something will carry its value from one <CodeEditor> to another.
minimap: {
enabled: false,
},
scrollBeyondLastLine: false,
automaticLayout: true,
fixedOverflowWidgets: true,
wordWrap: 'off',
lineNumbers: 'on',
...options,
Expand All @@ -33,6 +58,17 @@ export function CodeEditor({ options, ...editorProps }: CodeEditorProps): JSX.El
},
}}
{...editorProps}
onMount={(editor, monaco) => {
if (editorProps?.language === 'hog') {
registerHog(monaco)
}
if (editorProps?.language === 'hogql') {
registerHogQL(monaco)
}
if (onMount) {
onMount(editor, monaco)
}
}}
/>
)
}
Expand Down
245 changes: 245 additions & 0 deletions frontend/src/lib/monaco/hog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
// eslint-disable-next-line eslint-comments/disable-enable-pair
/* eslint-disable no-useless-escape */

// Adapted from: https://raw.githubusercontent.com/microsoft/monaco-editor/main/src/basic-languages/typescript/typescript.ts

import { languages } from 'monaco-editor'
export const conf: languages.LanguageConfiguration = {
wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,

comments: {
lineComment: '//',
blockComment: ['/*', '*/'],
},

brackets: [
['{', '}'],
['[', ']'],
['(', ')'],
],

onEnterRules: [
{
// e.g. /** | */
beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,
afterText: /^\s*\*\/$/,
action: {
indentAction: languages.IndentAction.IndentOutdent,
appendText: ' * ',
},
},
{
// e.g. /** ...|
beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,
action: {
indentAction: languages.IndentAction.None,
appendText: ' * ',
},
},
{
// e.g. * ...|
beforeText: /^(\t|(\ \ ))*\ \*(\ ([^\*]|\*(?!\/))*)?$/,
action: {
indentAction: languages.IndentAction.None,
appendText: '* ',
},
},
{
// e.g. */|
beforeText: /^(\t|(\ \ ))*\ \*\/\s*$/,
action: {
indentAction: languages.IndentAction.None,
removeText: 1,
},
},
],

autoClosingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '"', close: '"', notIn: ['string'] },
{ open: "'", close: "'", notIn: ['string', 'comment'] },
{ open: '`', close: '`', notIn: ['string', 'comment'] },
{ open: '/**', close: ' */', notIn: ['string'] },
],

folding: {
markers: {
start: new RegExp('^\\s*//\\s*#?region\\b'),
end: new RegExp('^\\s*//\\s*#?endregion\\b'),
},
},
}

export const language: languages.IMonarchLanguage = {
// Set defaultToken to invalid to see what you do not tokenize yet
defaultToken: 'invalid',
tokenPostfix: '.hog',

keywords: [
'fn',
'let',
'if',
'else',
'return',
'true',
'false',
'null',
'for',
'while',
'like',
'ilike',
'not',
'and',
'or',
'in',
],
operators: [
'<=',
'>=',
'==',
'!=',
'=>',
'+',
'-',
'**',
'*',
'/',
'%',
'<<',
'</',
'>>',
'>>>',
'&',
'|',
'^',
'!',
'~',
'||',
'??',
'?',
':',
'=',
':=',
'+=',
'-=',
'*=',
'*=~',
'!=',
'!=~',
],

// we include these common regular expressions
symbols: /[=><!~?:&|+\-*\/\^%]+/,
escapes: /\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,
digits: /\d+(_+\d+)*/,

// The main tokenizer for our languages
tokenizer: {
root: [[/[{}]/, 'delimiter.bracket'], { include: 'common' }],

common: [
// whitespace
{ include: '@whitespace' },

// delimiters and operators
[/[()\[\]]/, '@brackets'],
[/[<>](?!@symbols)/, '@brackets'],
[/!(?=([^=]|$))/, 'delimiter'],
[
/@symbols/,
{
cases: {
'@operators': 'delimiter',
'@default': '',
},
},
],

// numbers
[/(@digits)[eE]([\-+]?(@digits))?/, 'number.float'],
[/(@digits)\.(@digits)([eE][\-+]?(@digits))?/, 'number.float'],
[/(@digits)n?/, 'number'],

// delimiter: after number because of .\d floats
[/[;,.]/, 'delimiter'],

// strings that are actually fields, show as type.identifier to highlight
[/"([^"\\]|\\.)*$/, 'type.identifier.invalid'], // non-teminated type.identifier
[/'([^'\\]|\\.)*$/, 'type.identifier.invalid'], // non-teminated type.identifier
[/"/, 'type.identifier', '@string_double'],
[/`/, 'type.identifier', '@string_backtick'],

// strings
[/f'/, 'string', '@string_format'],
[/'/, 'string', '@string_single'],

// identifiers and keywords
[
/#?[a-z_$][\w$]*/,
{
cases: {
'@keywords': 'keyword',
'@default': 'identifier',
},
},
],
],

whitespace: [
[/[ \t\r\n]+/, ''],
[/\/\*\*(?!\/)/, 'comment.doc', '@jsdoc'],
[/\/\*/, 'comment', '@comment'],
[/\/\/.*$/, 'comment'],
[/--.*$/, 'comment'],
],

comment: [
[/[^\/*]+/, 'comment'],
[/\*\//, 'comment', '@pop'],
[/[\/*]/, 'comment'],
],

jsdoc: [
[/[^\/*]+/, 'comment.doc'],
[/\*\//, 'comment.doc', '@pop'],
[/[\/*]/, 'comment.doc'],
],

string_double: [
[/[^\\"]+/, 'type.identifier'],
[/@escapes/, 'type.identifier.escape'],
[/\\./, 'type.identifier.escape.invalid'],
[/"/, 'type.identifier', '@pop'],
],

string_backtick: [
[/[^\\`]+/, 'type.identifier'],
[/@escapes/, 'type.identifier.escape'],
[/\\./, 'type.identifier.escape.invalid'],
[/`/, 'type.identifier', '@pop'],
],

string_single: [
[/[^\\']+/, 'string'],
[/@escapes/, 'string.escape'],
[/\\./, 'string.escape.invalid'],
[/'/, 'string', '@pop'],
],

string_format: [
[/\{/, { token: 'delimiter.bracket', next: '@bracketCounting' }],
[/[^\\'{]+/, 'string'],
[/@escapes/, 'string.escape'],
[/\\./, 'string.escape.invalid'],
[/'/, 'string', '@pop'],
],

bracketCounting: [
[/\{/, 'delimiter.bracket', '@bracketCounting'],
[/\}/, 'delimiter.bracket', '@pop'],
{ include: 'common' },
],
},
}
Loading

0 comments on commit 5ec6d44

Please sign in to comment.