From 74378f74bfcc0428597efeda4f113e462fe90090 Mon Sep 17 00:00:00 2001 From: undergroundwires Date: Thu, 10 Oct 2024 17:16:32 +0200 Subject: [PATCH] Hide code highlight and cursor until interaction By default, the code area displays line highlighting (active line and gutter indicators) and shows the cursor even before user interaction, which clutters the initial UI. This commit hides the highlighting and cursor until the user interacts with the code area, providing a cleaner initial UI similar to modern editors when code is first displayed. When code is generated automatically, the code is already highlighted with a custom marker. Therefore, indicating a specific line by default is unnecessary and clutters the view. Key changes: - Hide active line highlighting before user interaction. - Hide cursor before user interaction. Other supporting changes: - Refactor `TheCodeArea` to extract third-party component (`Ace`) related logic for better maintainability. - Simplify code editor theme setting by removing the component property. - Remove unnecessary `github` theme import for the code editor component as it's unused. --- .../Code/Ace/AceCodeEditorFactory.ts | 91 +++++++++++++++++++ .../components/Code/{ => Ace}/ace-importer.ts | 1 - .../components/Code/CodeEditorFactory.ts | 30 ++++++ .../components/Code/TheCodeArea.vue | 74 ++++----------- .../components/Scripts/TheScriptArea.vue | 2 +- 5 files changed, 141 insertions(+), 57 deletions(-) create mode 100644 src/presentation/components/Code/Ace/AceCodeEditorFactory.ts rename src/presentation/components/Code/{ => Ace}/ace-importer.ts (93%) create mode 100644 src/presentation/components/Code/CodeEditorFactory.ts diff --git a/src/presentation/components/Code/Ace/AceCodeEditorFactory.ts b/src/presentation/components/Code/Ace/AceCodeEditorFactory.ts new file mode 100644 index 00000000..b18fc6ec --- /dev/null +++ b/src/presentation/components/Code/Ace/AceCodeEditorFactory.ts @@ -0,0 +1,91 @@ +import ace from './ace-importer'; +import type { CodeEditorFactory, SupportedSyntaxLanguage } from '../CodeEditorFactory'; + +const CodeEditorTheme = 'xcode'; + +export const initializeAceEditor: CodeEditorFactory = (options) => { + const editor = ace.edit(options.editorContainerElementId); + const mode = getAceModeName(options.language); + editor.getSession().setMode(`ace/mode/${mode}`); + editor.setTheme(`ace/theme/${CodeEditorTheme}`); + editor.setReadOnly(true); + editor.setAutoScrollEditorIntoView(true); + editor.setShowPrintMargin(false); // Hide the vertical line + editor.getSession().setUseWrapMode(true); // Make code readable on mobile + hideActiveLineAndCursorUntilInteraction(editor); + return { + setContent: (content) => editor.setValue(content, 1), + destroy: () => editor.destroy(), + scrollToLine: (lineNumber) => { + const column = editor.session.getLine(lineNumber).length; + if (column === undefined) { + return; + } + editor.gotoLine(lineNumber, column, true); + }, + updateSize: () => editor?.resize(), + applyStyleToLineRange: (start, end, className) => { + const AceRange = ace.require('ace/range').Range; + const markerId = editor.session.addMarker( + new AceRange(start, 0, end, 0), + className, + 'fullLine', + ); + return { + clearStyle: () => { + editor.session.removeMarker(markerId); + }, + }; + }, + }; +}; + +function getAceModeName(language: SupportedSyntaxLanguage): string { + switch (language) { + case 'batchfile': return 'batchfile'; + case 'shellscript': return 'sh'; + default: + throw new Error(`Language not supported: ${language}`); + } +} + +function hideActiveLineAndCursorUntilInteraction(editor: ace.Ace.Editor) { + hideActiveLineAndCursor(editor); + editor.session.on('change', () => { + editor.session.selection.clearSelection(); + hideActiveLineAndCursor(editor); + }); + editor.session.selection.on('changeSelection', () => { + showActiveLineAndCursor(editor); + }); +} + +function hideActiveLineAndCursor(editor: ace.Ace.Editor): void { + editor.setHighlightGutterLine(false); // Remove highlighting on line number column + editor.setHighlightActiveLine(false); // Remove highlighting throughout the line + setCursorVisibility(false, editor); +} + +function showActiveLineAndCursor(editor: ace.Ace.Editor): void { + editor.setHighlightGutterLine(true); // Show highlighting on line number column + editor.setHighlightActiveLine(true); // Show highlighting throughout the line + setCursorVisibility(true, editor); +} + +// Shows/removes vertical line after focused character +function setCursorVisibility( + isVisible: boolean, + editor: ace.Ace.Editor, +) { + const cursor = editor.renderer.container.querySelector('.ace_cursor-layer') as HTMLElement; + if (!cursor) { + throw new Error('Cannot find Ace cursor, did Ace change its rendering?'); + } + cursor.style.display = isVisible ? '' : 'none'; + // Implementation options for cursor visibility: + // ❌ editor.renderer.showCursor() and hideCursor(): Not functioning as expected + // ❌ editor.renderer.#cursorLayer: No longer part of the public API + // ✅ .ace_hidden-cursors { opacity: 0; }: Hides cursor when not focused + // Pros: Works more automatically + // Cons: Provides less control over visibility toggling +} diff --git a/src/presentation/components/Code/ace-importer.ts b/src/presentation/components/Code/Ace/ace-importer.ts similarity index 93% rename from src/presentation/components/Code/ace-importer.ts rename to src/presentation/components/Code/Ace/ace-importer.ts index 76978582..155b1ddf 100644 --- a/src/presentation/components/Code/ace-importer.ts +++ b/src/presentation/components/Code/Ace/ace-importer.ts @@ -5,7 +5,6 @@ import ace from 'ace-builds'; when built with Vite (`npm run build`). */ -import 'ace-builds/src-noconflict/theme-github'; import 'ace-builds/src-noconflict/theme-xcode'; import 'ace-builds/src-noconflict/mode-batchfile'; import 'ace-builds/src-noconflict/mode-sh'; diff --git a/src/presentation/components/Code/CodeEditorFactory.ts b/src/presentation/components/Code/CodeEditorFactory.ts new file mode 100644 index 00000000..14ed8993 --- /dev/null +++ b/src/presentation/components/Code/CodeEditorFactory.ts @@ -0,0 +1,30 @@ +/** + * Abstraction layer for code editor functionality. + * Allows for flexible integration and easy switching of third-party editor implementations. + */ +export interface CodeEditorFactory { + (options: CodeEditorOptions): CodeEditor; +} + +export interface CodeEditorOptions { + readonly editorContainerElementId: string; + readonly language: SupportedSyntaxLanguage; +} + +export type SupportedSyntaxLanguage = 'batchfile' | 'shellscript'; + +export interface CodeEditor { + destroy(): void; + setContent(content: string): void; + scrollToLine(lineNumber: number): void; + updateSize(): void; + applyStyleToLineRange( + startLineNumber: number, + endLineNumber: number, + className: string, + ): CodeEditorStyleHandle; +} + +export interface CodeEditorStyleHandle { + clearStyle(): void; +} diff --git a/src/presentation/components/Code/TheCodeArea.vue b/src/presentation/components/Code/TheCodeArea.vue index 93edd884..65869690 100644 --- a/src/presentation/components/Code/TheCodeArea.vue +++ b/src/presentation/components/Code/TheCodeArea.vue @@ -25,7 +25,8 @@ import { CodeBuilderFactory } from '@/application/Context/State/Code/Generation/ import SizeObserver from '@/presentation/components/Shared/SizeObserver.vue'; import { NonCollapsing } from '@/presentation/components/Scripts/View/Cards/NonCollapsingDirective'; import type { ProjectDetails } from '@/domain/Project/ProjectDetails'; -import ace from './ace-importer'; +import { initializeAceEditor } from './Ace/AceCodeEditorFactory'; +import type { SupportedSyntaxLanguage, CodeEditor, CodeEditorStyleHandle } from './CodeEditorFactory'; export default defineComponent({ components: { @@ -34,13 +35,7 @@ export default defineComponent({ directives: { NonCollapsing, }, - props: { - theme: { - type: String, - default: undefined, - }, - }, - setup(props) { + setup() { const { onStateChange, currentState } = injectKey((keys) => keys.useCollectionState); const { projectDetails } = injectKey((keys) => keys.useApplication); const { events } = injectKey((keys) => keys.useAutoUnsubscribedEvents); @@ -48,8 +43,8 @@ export default defineComponent({ const editorId = 'codeEditor'; const highlightedRange = ref(0); - let editor: ace.Ace.Editor | undefined; - let currentMarkerId: number | undefined; + let editor: CodeEditor | undefined; + let currentMarker: CodeEditorStyleHandle | undefined; onUnmounted(() => { destroyEditor(); @@ -63,11 +58,10 @@ export default defineComponent({ function handleNewState(newState: IReadOnlyCategoryCollectionState) { destroyEditor(); - editor = initializeEditor( - props.theme, - editorId, - newState.collection.scripting.language, - ); + editor = initializeAceEditor({ + editorContainerElementId: editorId, + language: getLanguage(newState.collection.scripting.language), + }); const appCode = newState.code; updateCode(appCode.current, newState.collection.scripting.language); events.unsubscribeAllAndRegister([ @@ -77,7 +71,7 @@ export default defineComponent({ function updateCode(code: string, language: ScriptingLanguage) { const innerCode = code || getDefaultCode(language, projectDetails); - editor?.setValue(innerCode, 1); + editor?.setContent(innerCode); } function handleCodeChange(event: ICodeChangedEvent) { @@ -91,7 +85,7 @@ export default defineComponent({ } function sizeChanged() { - editor?.resize(); + editor?.updateSize(); } function destroyEditor() { @@ -100,11 +94,11 @@ export default defineComponent({ } function removeCurrentHighlighting() { - if (!currentMarkerId) { + if (!currentMarker) { return; } - editor?.session.removeMarker(currentMarkerId); - currentMarkerId = undefined; + currentMarker?.clearStyle(); + currentMarker = undefined; highlightedRange.value = 0; } @@ -117,28 +111,15 @@ export default defineComponent({ const end = Math.max( ...positions.map((position) => position.endLine), ); - scrollToLine(end + 2); + editor?.scrollToLine(end + 2); highlight(start, end); } function highlight(startRow: number, endRow: number) { - const AceRange = ace.require('ace/range').Range; - currentMarkerId = editor?.session.addMarker( - new AceRange(startRow, 0, endRow, 0), - 'code-area__highlight', - 'fullLine', - ); + currentMarker = editor?.applyStyleToLineRange(startRow, endRow, 'code-area__highlight'); highlightedRange.value = endRow - startRow; } - function scrollToLine(row: number) { - const column = editor?.session.getLine(row).length; - if (column === undefined) { - return; - } - editor?.gotoLine(row, column, true); - } - return { editorId, highlightedRange, @@ -147,29 +128,12 @@ export default defineComponent({ }, }); -function initializeEditor( - theme: string | undefined, - editorId: string, - language: ScriptingLanguage, -): ace.Ace.Editor { - theme = theme || 'github'; - const editor = ace.edit(editorId); - const lang = getLanguage(language); - editor.getSession().setMode(`ace/mode/${lang}`); - editor.setTheme(`ace/theme/${theme}`); - editor.setReadOnly(true); - editor.setAutoScrollEditorIntoView(true); - editor.setShowPrintMargin(false); // hides vertical line - editor.getSession().setUseWrapMode(true); // So code is readable on mobile - return editor; -} - -function getLanguage(language: ScriptingLanguage) { +function getLanguage(language: ScriptingLanguage): SupportedSyntaxLanguage { switch (language) { case ScriptingLanguage.batchfile: return 'batchfile'; - case ScriptingLanguage.shellscript: return 'sh'; + case ScriptingLanguage.shellscript: return 'shellscript'; default: - throw new Error('unknown language'); + throw new Error(`Unsupported language: ${language}`); } } diff --git a/src/presentation/components/Scripts/TheScriptArea.vue b/src/presentation/components/Scripts/TheScriptArea.vue index 0a88456e..01411f6e 100644 --- a/src/presentation/components/Scripts/TheScriptArea.vue +++ b/src/presentation/components/Scripts/TheScriptArea.vue @@ -11,7 +11,7 @@