Skip to content

Commit

Permalink
feat: basic Go.Mod syntax support (#430)
Browse files Browse the repository at this point in the history
* feat: basic Go.Mod syntax support

* chore: update changelog
  • Loading branch information
x1unix authored Oct 6, 2024
1 parent e1aa5e4 commit 7875d91
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 14 deletions.
17 changes: 11 additions & 6 deletions web/src/changelog.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
{
"Interface - General": [
{
"issueId": 425,
"url": "pull/425",
"description": "Reduce application bundle size"
}
],
"Interface - Editor": [
{
"issueId": 417,
Expand All @@ -9,13 +16,11 @@
"issueId": 421,
"url": "pull/421",
"description": "Show symbol documentation on hover"
}
],
"Interface - General": [
},
{
"issueId": 425,
"url": "pull/425",
"description": "Reduce application bundle size"
"issueId": 430,
"url": "pull/430",
"description": "Syntax highlight support for go.mod files"
}
],
"Go - WebAssembly": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ import type { VimState } from '~/store/vim/state'
import { spawnLanguageWorker } from '~/workers/language'
import { getTimeNowUsageMarkers, asyncDebounce, debounce } from './utils/utils'
import { attachCustomCommands } from './utils/commands'
import { languageFromFilename, stateToOptions } from './utils/props'
import { stateToOptions } from './utils/props'
import { configureMonacoLoader } from './utils/loader'
import { DocumentMetadataCache, registerGoLanguageProviders } from './autocomplete'
import { languageFromFilename, registerExtraLanguages } from './grammar'
import classes from './CodeEditor.module.css'

const ANALYZE_DEBOUNCE_TIME = 500
Expand Down Expand Up @@ -74,6 +75,7 @@ class CodeEditorView extends React.Component<Props> {
editorDidMount(editorInstance: monaco.editor.IStandaloneCodeEditor, monacoInstance: Monaco) {
const [langWorker, workerDisposer] = spawnLanguageWorker()

this.addDisposer(registerExtraLanguages())
this.addDisposer(workerDisposer)
this.addDisposer(...registerGoLanguageProviders(this.props.dispatch, this.metadataCache, langWorker))
this.editorInstance = editorInstance
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@ import * as monaco from 'monaco-editor'
import { GoSymbolsCompletionItemProvider } from './symbols'
import { GoImportsCompletionProvider } from './imports'
import { GoHoverProvider } from './hover'
import { LanguageID } from '../grammar'
import type { StateDispatch } from '~/store'
import type { DocumentMetadataCache } from './cache'
import type { LanguageWorker } from '~/workers/language'

const LANG_GO = 'go'

/**
* Registers all Go autocomplete providers for Monaco editor.
*/
Expand All @@ -19,13 +18,13 @@ export const registerGoLanguageProviders = (
) => {
return [
monaco.languages.registerCompletionItemProvider(
LANG_GO,
LanguageID.Go,
new GoSymbolsCompletionItemProvider(dispatcher, cache, langWorker),
),
monaco.languages.registerCompletionItemProvider(
LANG_GO,
LanguageID.Go,
new GoImportsCompletionProvider(dispatcher, cache, langWorker),
),
monaco.languages.registerHoverProvider(LANG_GO, new GoHoverProvider(langWorker, cache)),
monaco.languages.registerHoverProvider(LanguageID.Go, new GoHoverProvider(langWorker, cache)),
]
}
128 changes: 128 additions & 0 deletions web/src/components/features/workspace/CodeEditor/grammar/gomod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import type { languages } from 'monaco-editor'

export const conf: languages.LanguageConfiguration = {
comments: {
lineComment: '//',
},
brackets: [['(', ')']],
autoClosingPairs: [
{ open: '(', close: ')' },
{ open: '"', close: '"', notIn: ['string'] },
{ open: '`', close: '`', notIn: ['string'] },
],
surroundingPairs: [
{ open: '(', close: ')' },
{ open: '"', close: '"' },
{ open: '`', close: '`' },
],
}

export const language: languages.IMonarchLanguage = {
defaultToken: '',
tokenPostfix: '.go.mod',

keywords: ['module', 'require', 'replace', 'exclude', 'go', 'toolchain'],
operators: ['=>'],

tokenizer: {
root: [
// Comments
[/\/\/.*$/, 'comment'],

// Multi-Line Directive
[
/^(\w+)(\s*)(\()/,
[
{ cases: { '@keywords': 'keyword', '@default': 'identifier' }, next: '@directiveArgs' },
'white',
'delimiter.parenthesis',
],
],

// Single-Line Directive
[
/^(\w+)(\s*)(.*)$/,
[{ cases: { '@keywords': 'keyword', '@default': 'identifier' } }, 'white', { token: '', next: '@arguments' }],
],

// Invalid
[/.*$/, 'invalid'],
],

directiveArgs: [{ include: '@arguments' }, [/\)/, { token: 'delimiter.parenthesis', next: '@pop' }]],

arguments: [
// Comments
[/\/\/.*$/, 'comment'],

// Double Quoted String
[/"([^"\\]|\\.)*$/, 'string.invalid'], // Non-terminated string
[/"/, { token: 'string.quote', next: '@doubleString' }],

// Raw Quoted String
[/`/, { token: 'string.quote', next: '@rawString' }],
// Operator
[/=>/, 'operator'],
// Semver Version
[
/v(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*)?(?:\+[\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*)?/,
'number',
],
// Unquoted String
[/([^\s/]|\/(?!\/))+/, 'string'],
// Whitespace
[/\s+/, 'white'],
// End of Line
[/$/, '', '@pop'],
],
doubleString: [
// Escaped Characters
{ include: '@stringEscapedChar' },
// Placeholders
{ include: '@stringPlaceholder' },
// Regular String Content
[/[^\\%"']+/, 'string'],
// Escape at End of Line
[/\\$/, 'string.escape'],
// Closing Quote
[/"/, { token: 'string.quote', next: '@pop' }],
// Invalid Escape
[/\\./, 'string.escape.invalid'],
],
rawString: [
// Placeholders
{ include: '@stringPlaceholder' },
// Regular String Content
[/[^%`]+/, 'string'],

// Percent Sign Not Followed by Placeholder
[/%%/, 'string'],
[/%/, 'string'],

// Closing Backtick
[/`/, { token: 'string.quote', next: '@pop' }],
],

stringEscapedChar: [
[/\\([0-7]{3}|[abfnrtv\\"']|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/, 'string.escape'],
[/\\./, 'invalid'],
],

stringPlaceholder: [
[/%(\[\d+])?([+#\-0\x20]{0,2}((\d+|\*)?(\.?(?:\d+|\*|\[\d+]\*?)?\[\d+]?)?))?[vT%tbcdoqxXUeEfFgGsp]/, 'variable'],
],
},
}
22 changes: 22 additions & 0 deletions web/src/components/features/workspace/CodeEditor/grammar/ids.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export enum LanguageID {
Go = 'go',
GoMod = 'go.mod',
GoSum = 'go.sum',
}

const basename = (filepath: string) => {
const slashPos = filepath.lastIndexOf('/')
return slashPos === -1 ? filepath : filepath.slice(slashPos + 1)
}

export const languageFromFilename = (filepath: string) => {
const fname = basename(filepath)
switch (fname) {
case 'go.mod':
return LanguageID.GoMod
case 'go.sum':
return LanguageID.GoSum
default:
return fname.endsWith('.go') ? LanguageID.Go : 'plaintext'
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './ids'
export * from './register'
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as monaco from 'monaco-editor'

import { LanguageID } from './ids'

const isLangRegistered = (langId: string) => {
// Monaco doesn't provide quick search
const match = monaco.languages.getLanguages().filter(({ id }) => id === langId)
return !!match.length
}

const concatDisposables = (...items: monaco.IDisposable[]): monaco.IDisposable => ({
dispose: () => {
items.forEach((i) => i.dispose())
},
})

export const registerExtraLanguages = (): monaco.IDisposable => {
console.log('register')
if (!isLangRegistered(LanguageID.GoMod)) {
monaco.languages.register({
id: 'go.mod',
extensions: ['.mod'],
filenames: ['go.mod'],
aliases: ['GoMod'],
})
}

return concatDisposables(
monaco.languages.registerTokensProviderFactory(LanguageID.GoMod, {
create: async () => {
const mod = await import('./gomod.ts')
return mod.language
},
}),

monaco.languages.onLanguageEncountered(LanguageID.GoMod, async () => {
const mod = await import('./gomod.ts')
monaco.languages.setLanguageConfiguration(LanguageID.GoMod, mod.conf)
}),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import type * as monaco from 'monaco-editor'
import { type MonacoSettings } from '~/services/config'
import { getFontFamily, getDefaultFontFamily } from '~/services/fonts'

export const languageFromFilename = (fname: string) => (fname.endsWith('.mod') ? 'gomod' : 'go')

// stateToOptions converts MonacoState to IEditorOptions
export const stateToOptions = (state: MonacoSettings): monaco.editor.IEditorOptions => {
// fontSize here is intentionally ignored as monaco-editor starts to infinitly change
Expand Down

0 comments on commit 7875d91

Please sign in to comment.