diff --git a/web/public/fonts/Recursive_VF_1.085.woff2 b/web/public/fonts/Recursive_VF_1.085.woff2 deleted file mode 100644 index 0f4358fe..00000000 Binary files a/web/public/fonts/Recursive_VF_1.085.woff2 and /dev/null differ diff --git a/web/src/components/features/workspace/CodeEditor/CodeEditor.tsx b/web/src/components/features/workspace/CodeEditor/CodeEditor.tsx index 3e370a28..b33fcced 100644 --- a/web/src/components/features/workspace/CodeEditor/CodeEditor.tsx +++ b/web/src/components/features/workspace/CodeEditor/CodeEditor.tsx @@ -1,15 +1,21 @@ import React from 'react' import { Spinner } from '@fluentui/react' import MonacoEditor, { type Monaco } from '@monaco-editor/react' -import { KeyMod, KeyCode, type editor, type IKeyboardEvent, type IDisposable } from 'monaco-editor' +import * as monaco from 'monaco-editor' import apiClient from '~/services/api' import { createVimModeAdapter, type StatusBarAdapter, type VimModeKeymap } from '~/plugins/vim/editor' import { Analyzer } from '~/services/analyzer' import { type MonacoSettings, TargetType } from '~/services/config' -import { connect, newMarkerAction, runFileDispatcher, type StateDispatch } from '~/store' +import { + connect, + newMarkerAction, + newMonacoParamsChangeDispatcher, + runFileDispatcher, + type StateDispatch, +} from '~/store' import { type WorkspaceState, dispatchFormatFile, dispatchResetWorkspace, dispatchUpdateFile } from '~/store/workspace' -import { getTimeNowUsageMarkers, wrapAsyncWithDebounce } from './utils' +import { getTimeNowUsageMarkers, asyncDebounce, debounce } from './utils' import { attachCustomCommands } from './commands' import { LANGUAGE_GOLANG, stateToOptions } from './props' import { configureMonacoLoader } from './loader' @@ -52,17 +58,25 @@ interface Props extends CodeEditorState { class CodeEditor extends React.Component { private analyzer?: Analyzer - private editorInstance?: editor.IStandaloneCodeEditor + private editorInstance?: monaco.editor.IStandaloneCodeEditor private vimAdapter?: VimModeKeymap private vimCommandAdapter?: StatusBarAdapter private monaco?: Monaco - private disposables?: IDisposable[] + private disposables?: monaco.IDisposable[] - private readonly debouncedAnalyzeFunc = wrapAsyncWithDebounce(async (fileName: string, code: string) => { + private readonly debouncedAnalyzeFunc = asyncDebounce(async (fileName: string, code: string) => { return await this.doAnalyze(fileName, code) }, ANALYZE_DEBOUNCE_TIME) - editorDidMount(editorInstance: editor.IStandaloneCodeEditor, monacoInstance: Monaco) { + private readonly persistFontSize = debounce((fontSize: number) => { + this.props.dispatch( + newMonacoParamsChangeDispatcher({ + fontSize, + }), + ) + }, 1000) + + editorDidMount(editorInstance: monaco.editor.IStandaloneCodeEditor, monacoInstance: Monaco) { this.disposables = registerGoLanguageProviders(apiClient, this.props.dispatch) this.editorInstance = editorInstance this.monaco = monacoInstance @@ -72,6 +86,15 @@ class CodeEditor extends React.Component { this.vimAdapter = vimAdapter this.vimCommandAdapter = statusAdapter + // Font should be set only once during boot as when font size changes + // by zoom and editor config object is updated - this cause infinite + // font change calls with random values. + if (this.props.options.fontSize) { + editorInstance.updateOptions({ + fontSize: this.props.options.fontSize, + }) + } + if (this.props.vimModeEnabled) { console.log('Vim mode enabled') this.vimAdapter.attach() @@ -96,7 +119,7 @@ class CodeEditor extends React.Component { id: 'run-code', label: 'Build And Run Code', contextMenuGroupId: 'navigation', - keybindings: [KeyMod.CtrlCmd | KeyCode.Enter], + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter], run: (ed, ...args) => { this.props.dispatch(runFileDispatcher) }, @@ -105,13 +128,23 @@ class CodeEditor extends React.Component { id: 'format-code', label: 'Format Code (goimports)', contextMenuGroupId: 'navigation', - keybindings: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyF], + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyF], run: (ed, ...args) => { this.props.dispatch(dispatchFormatFile()) }, }, ] + // Persist font size on zoom + this.disposables.push( + editorInstance.onDidChangeConfiguration((e) => { + if (e.hasChanged(monaco.editor.EditorOption.fontSize)) { + const newFontSize = editorInstance.getOption(monaco.editor.EditorOption.fontSize) + this.persistFontSize(newFontSize) + } + }), + ) + // Register custom actions actions.forEach((action) => editorInstance.addAction(action)) attachCustomCommands(editorInstance) @@ -166,7 +199,7 @@ class CodeEditor extends React.Component { this.editorInstance.dispose() } - onChange(newValue: string | undefined, _: editor.IModelContentChangedEvent) { + onChange(newValue: string | undefined, _: monaco.editor.IModelContentChangedEvent) { if (!newValue) { return } @@ -206,7 +239,7 @@ class CodeEditor extends React.Component { this.props.dispatch(newMarkerAction(fileName, markers)) } - private onKeyDown(e: IKeyboardEvent) { + private onKeyDown(e: monaco.IKeyboardEvent) { const { vimModeEnabled, vim } = this.props if (!vimModeEnabled || !vim?.commandStarted) { return diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/provider.ts b/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/provider.ts index 96f9f25c..5f8eb66d 100644 --- a/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/provider.ts +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/provider.ts @@ -1,6 +1,6 @@ import type * as monaco from 'monaco-editor' import { type IAPIClient } from '~/services/api' -import { wrapAsyncWithDebounce } from '../../utils' +import { asyncDebounce } from '../../utils' import snippets from './snippets' import { parseExpression } from './parse' @@ -10,7 +10,7 @@ export class GoCompletionItemProvider implements monaco.languages.CompletionItem private readonly getSuggestionFunc: IAPIClient['getSuggestions'] constructor(private readonly client: IAPIClient) { - this.getSuggestionFunc = wrapAsyncWithDebounce( + this.getSuggestionFunc = asyncDebounce( async (query) => await client.getSuggestions(query), SUGGESTIONS_DEBOUNCE_DELAY, ) diff --git a/web/src/components/features/workspace/CodeEditor/props.ts b/web/src/components/features/workspace/CodeEditor/props.ts index a52a4a82..a79c8afb 100644 --- a/web/src/components/features/workspace/CodeEditor/props.ts +++ b/web/src/components/features/workspace/CodeEditor/props.ts @@ -16,6 +16,10 @@ export const DEMO_CODE = [ // 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 + // font size to random values for unknown reason. + // + // Font size apply moved to componentDidMount. const { selectOnLineNumbers, mouseWheelZoom, diff --git a/web/src/components/features/workspace/CodeEditor/utils.ts b/web/src/components/features/workspace/CodeEditor/utils.ts index aa82831c..232c5b99 100644 --- a/web/src/components/features/workspace/CodeEditor/utils.ts +++ b/web/src/components/features/workspace/CodeEditor/utils.ts @@ -55,7 +55,7 @@ export const getTimeNowUsageMarkers = ( * @param fn Function * @param delay Debounce time */ -export const wrapAsyncWithDebounce = (fn: (...args) => Promise, delay: number) => { +export const asyncDebounce = (fn: (...args) => Promise, delay: number) => { let lastTimeoutId: NodeJS.Timeout | null = null return async (...args) => { @@ -72,3 +72,18 @@ export const wrapAsyncWithDebounce = (fn: (...args) => Promise, delay: num }) } } + +/** + * Wraps passed function with a debouncer + */ +export const debounce = (fn: (...args) => T, delay: number) => { + let lastTimeoutId: NodeJS.Timeout | null = null + + return (...args) => { + if (lastTimeoutId) { + clearTimeout(lastTimeoutId) + } + + lastTimeoutId = setTimeout(() => fn(...args), delay) + } +} diff --git a/web/src/services/config/monaco.ts b/web/src/services/config/monaco.ts index 411908bf..73dca94b 100644 --- a/web/src/services/config/monaco.ts +++ b/web/src/services/config/monaco.ts @@ -2,6 +2,7 @@ import { DEFAULT_FONT } from '../fonts' export interface MonacoSettings { fontFamily: string + fontSize?: number fontLigatures: boolean cursorBlinking: 'blink' | 'smooth' | 'phase' | 'expand' | 'solid' cursorStyle: 'line' | 'block' | 'underline' | 'line-thin' | 'block-outline' | 'underline-thin' @@ -13,6 +14,7 @@ export interface MonacoSettings { } export const defaultMonacoSettings: MonacoSettings = { + fontSize: 12, fontFamily: DEFAULT_FONT, fontLigatures: false, cursorBlinking: 'blink', diff --git a/web/src/services/fonts.ts b/web/src/services/fonts.ts index 76b6fc13..5559c72c 100644 --- a/web/src/services/fonts.ts +++ b/web/src/services/fonts.ts @@ -165,15 +165,6 @@ const fontsList: Record = { }, ], }, - Recursive: { - label: 'Recursive', - family: 'Recursive', - fonts: [ - { - src: [{ url: '/fonts/Recursive_VF_1.085.woff2', format: 'woff2' }], - }, - ], - }, 'Zed-Mono': { label: 'Zed Mono', family: 'Zed Mono', diff --git a/web/src/store/actions/settings.ts b/web/src/store/actions/settings.ts index c96f51be..a31f6a48 100644 --- a/web/src/store/actions/settings.ts +++ b/web/src/store/actions/settings.ts @@ -3,9 +3,7 @@ import { type MonacoSettings, type RunTargetConfig } from '~/services/config' import { ActionType } from './actions' import { type PanelState, type SettingsState } from '../state' -export type MonacoParamsChanges = { - [k in keyof MonacoSettings | string]: T -} +export type MonacoParamsChanges = Partial export const newToggleThemeAction = () => ({ type: ActionType.TOGGLE_THEME, @@ -17,7 +15,7 @@ export const newRunTargetChangeAction = (cfg: RunTargetConfig) => ({ payload: cfg, }) -export const newMonacoParamsChangeAction = (changes: MonacoParamsChanges) => ({ +export const newMonacoParamsChangeAction = (changes: MonacoParamsChanges) => ({ type: ActionType.MONACO_SETTINGS_CHANGE, payload: changes, })