Skip to content

Commit 7875d91

Browse files
authoredOct 6, 2024··
feat: basic Go.Mod syntax support (#430)
* feat: basic Go.Mod syntax support * chore: update changelog
1 parent e1aa5e4 commit 7875d91

File tree

8 files changed

+211
-14
lines changed

8 files changed

+211
-14
lines changed
 

‎web/src/changelog.json

+11-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
{
2+
"Interface - General": [
3+
{
4+
"issueId": 425,
5+
"url": "pull/425",
6+
"description": "Reduce application bundle size"
7+
}
8+
],
29
"Interface - Editor": [
310
{
411
"issueId": 417,
@@ -9,13 +16,11 @@
916
"issueId": 421,
1017
"url": "pull/421",
1118
"description": "Show symbol documentation on hover"
12-
}
13-
],
14-
"Interface - General": [
19+
},
1520
{
16-
"issueId": 425,
17-
"url": "pull/425",
18-
"description": "Reduce application bundle size"
21+
"issueId": 430,
22+
"url": "pull/430",
23+
"description": "Syntax highlight support for go.mod files"
1924
}
2025
],
2126
"Go - WebAssembly": [

‎web/src/components/features/workspace/CodeEditor/CodeEditor.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ import type { VimState } from '~/store/vim/state'
1919
import { spawnLanguageWorker } from '~/workers/language'
2020
import { getTimeNowUsageMarkers, asyncDebounce, debounce } from './utils/utils'
2121
import { attachCustomCommands } from './utils/commands'
22-
import { languageFromFilename, stateToOptions } from './utils/props'
22+
import { stateToOptions } from './utils/props'
2323
import { configureMonacoLoader } from './utils/loader'
2424
import { DocumentMetadataCache, registerGoLanguageProviders } from './autocomplete'
25+
import { languageFromFilename, registerExtraLanguages } from './grammar'
2526
import classes from './CodeEditor.module.css'
2627

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

78+
this.addDisposer(registerExtraLanguages())
7779
this.addDisposer(workerDisposer)
7880
this.addDisposer(...registerGoLanguageProviders(this.props.dispatch, this.metadataCache, langWorker))
7981
this.editorInstance = editorInstance

‎web/src/components/features/workspace/CodeEditor/autocomplete/register.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@ import * as monaco from 'monaco-editor'
33
import { GoSymbolsCompletionItemProvider } from './symbols'
44
import { GoImportsCompletionProvider } from './imports'
55
import { GoHoverProvider } from './hover'
6+
import { LanguageID } from '../grammar'
67
import type { StateDispatch } from '~/store'
78
import type { DocumentMetadataCache } from './cache'
89
import type { LanguageWorker } from '~/workers/language'
910

10-
const LANG_GO = 'go'
11-
1211
/**
1312
* Registers all Go autocomplete providers for Monaco editor.
1413
*/
@@ -19,13 +18,13 @@ export const registerGoLanguageProviders = (
1918
) => {
2019
return [
2120
monaco.languages.registerCompletionItemProvider(
22-
LANG_GO,
21+
LanguageID.Go,
2322
new GoSymbolsCompletionItemProvider(dispatcher, cache, langWorker),
2423
),
2524
monaco.languages.registerCompletionItemProvider(
26-
LANG_GO,
25+
LanguageID.Go,
2726
new GoImportsCompletionProvider(dispatcher, cache, langWorker),
2827
),
29-
monaco.languages.registerHoverProvider(LANG_GO, new GoHoverProvider(langWorker, cache)),
28+
monaco.languages.registerHoverProvider(LanguageID.Go, new GoHoverProvider(langWorker, cache)),
3029
]
3130
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import type { languages } from 'monaco-editor'
2+
3+
export const conf: languages.LanguageConfiguration = {
4+
comments: {
5+
lineComment: '//',
6+
},
7+
brackets: [['(', ')']],
8+
autoClosingPairs: [
9+
{ open: '(', close: ')' },
10+
{ open: '"', close: '"', notIn: ['string'] },
11+
{ open: '`', close: '`', notIn: ['string'] },
12+
],
13+
surroundingPairs: [
14+
{ open: '(', close: ')' },
15+
{ open: '"', close: '"' },
16+
{ open: '`', close: '`' },
17+
],
18+
}
19+
20+
export const language: languages.IMonarchLanguage = {
21+
defaultToken: '',
22+
tokenPostfix: '.go.mod',
23+
24+
keywords: ['module', 'require', 'replace', 'exclude', 'go', 'toolchain'],
25+
operators: ['=>'],
26+
27+
tokenizer: {
28+
root: [
29+
// Comments
30+
[/\/\/.*$/, 'comment'],
31+
32+
// Multi-Line Directive
33+
[
34+
/^(\w+)(\s*)(\()/,
35+
[
36+
{ cases: { '@keywords': 'keyword', '@default': 'identifier' }, next: '@directiveArgs' },
37+
'white',
38+
'delimiter.parenthesis',
39+
],
40+
],
41+
42+
// Single-Line Directive
43+
[
44+
/^(\w+)(\s*)(.*)$/,
45+
[{ cases: { '@keywords': 'keyword', '@default': 'identifier' } }, 'white', { token: '', next: '@arguments' }],
46+
],
47+
48+
// Invalid
49+
[/.*$/, 'invalid'],
50+
],
51+
52+
directiveArgs: [{ include: '@arguments' }, [/\)/, { token: 'delimiter.parenthesis', next: '@pop' }]],
53+
54+
arguments: [
55+
// Comments
56+
[/\/\/.*$/, 'comment'],
57+
58+
// Double Quoted String
59+
[/"([^"\\]|\\.)*$/, 'string.invalid'], // Non-terminated string
60+
[/"/, { token: 'string.quote', next: '@doubleString' }],
61+
62+
// Raw Quoted String
63+
[/`/, { token: 'string.quote', next: '@rawString' }],
64+
65+
// Operator
66+
[/=>/, 'operator'],
67+
68+
// Semver Version
69+
[
70+
/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-]+)*)?/,
71+
'number',
72+
],
73+
74+
// Unquoted String
75+
[/([^\s/]|\/(?!\/))+/, 'string'],
76+
77+
// Whitespace
78+
[/\s+/, 'white'],
79+
80+
// End of Line
81+
[/$/, '', '@pop'],
82+
],
83+
84+
doubleString: [
85+
// Escaped Characters
86+
{ include: '@stringEscapedChar' },
87+
88+
// Placeholders
89+
{ include: '@stringPlaceholder' },
90+
91+
// Regular String Content
92+
[/[^\\%"']+/, 'string'],
93+
94+
// Escape at End of Line
95+
[/\\$/, 'string.escape'],
96+
97+
// Closing Quote
98+
[/"/, { token: 'string.quote', next: '@pop' }],
99+
100+
// Invalid Escape
101+
[/\\./, 'string.escape.invalid'],
102+
],
103+
104+
rawString: [
105+
// Placeholders
106+
{ include: '@stringPlaceholder' },
107+
108+
// Regular String Content
109+
[/[^%`]+/, 'string'],
110+
111+
// Percent Sign Not Followed by Placeholder
112+
[/%%/, 'string'],
113+
[/%/, 'string'],
114+
115+
// Closing Backtick
116+
[/`/, { token: 'string.quote', next: '@pop' }],
117+
],
118+
119+
stringEscapedChar: [
120+
[/\\([0-7]{3}|[abfnrtv\\"']|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/, 'string.escape'],
121+
[/\\./, 'invalid'],
122+
],
123+
124+
stringPlaceholder: [
125+
[/%(\[\d+])?([+#\-0\x20]{0,2}((\d+|\*)?(\.?(?:\d+|\*|\[\d+]\*?)?\[\d+]?)?))?[vT%tbcdoqxXUeEfFgGsp]/, 'variable'],
126+
],
127+
},
128+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export enum LanguageID {
2+
Go = 'go',
3+
GoMod = 'go.mod',
4+
GoSum = 'go.sum',
5+
}
6+
7+
const basename = (filepath: string) => {
8+
const slashPos = filepath.lastIndexOf('/')
9+
return slashPos === -1 ? filepath : filepath.slice(slashPos + 1)
10+
}
11+
12+
export const languageFromFilename = (filepath: string) => {
13+
const fname = basename(filepath)
14+
switch (fname) {
15+
case 'go.mod':
16+
return LanguageID.GoMod
17+
case 'go.sum':
18+
return LanguageID.GoSum
19+
default:
20+
return fname.endsWith('.go') ? LanguageID.Go : 'plaintext'
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './ids'
2+
export * from './register'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import * as monaco from 'monaco-editor'
2+
3+
import { LanguageID } from './ids'
4+
5+
const isLangRegistered = (langId: string) => {
6+
// Monaco doesn't provide quick search
7+
const match = monaco.languages.getLanguages().filter(({ id }) => id === langId)
8+
return !!match.length
9+
}
10+
11+
const concatDisposables = (...items: monaco.IDisposable[]): monaco.IDisposable => ({
12+
dispose: () => {
13+
items.forEach((i) => i.dispose())
14+
},
15+
})
16+
17+
export const registerExtraLanguages = (): monaco.IDisposable => {
18+
console.log('register')
19+
if (!isLangRegistered(LanguageID.GoMod)) {
20+
monaco.languages.register({
21+
id: 'go.mod',
22+
extensions: ['.mod'],
23+
filenames: ['go.mod'],
24+
aliases: ['GoMod'],
25+
})
26+
}
27+
28+
return concatDisposables(
29+
monaco.languages.registerTokensProviderFactory(LanguageID.GoMod, {
30+
create: async () => {
31+
const mod = await import('./gomod.ts')
32+
return mod.language
33+
},
34+
}),
35+
36+
monaco.languages.onLanguageEncountered(LanguageID.GoMod, async () => {
37+
const mod = await import('./gomod.ts')
38+
monaco.languages.setLanguageConfiguration(LanguageID.GoMod, mod.conf)
39+
}),
40+
)
41+
}

‎web/src/components/features/workspace/CodeEditor/utils/props.ts

-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ import type * as monaco from 'monaco-editor'
22
import { type MonacoSettings } from '~/services/config'
33
import { getFontFamily, getDefaultFontFamily } from '~/services/fonts'
44

5-
export const languageFromFilename = (fname: string) => (fname.endsWith('.mod') ? 'gomod' : 'go')
6-
75
// stateToOptions converts MonacoState to IEditorOptions
86
export const stateToOptions = (state: MonacoSettings): monaco.editor.IEditorOptions => {
97
// fontSize here is intentionally ignored as monaco-editor starts to infinitly change

0 commit comments

Comments
 (0)
Please sign in to comment.