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.
This commit is contained in:
undergroundwires
2024-10-10 17:16:32 +02:00
parent 2f31bc7b06
commit 74378f74bf
5 changed files with 141 additions and 57 deletions

View File

@@ -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
}

View File

@@ -5,7 +5,6 @@ import ace from 'ace-builds';
when built with Vite (`npm run build`). 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/theme-xcode';
import 'ace-builds/src-noconflict/mode-batchfile'; import 'ace-builds/src-noconflict/mode-batchfile';
import 'ace-builds/src-noconflict/mode-sh'; import 'ace-builds/src-noconflict/mode-sh';

View File

@@ -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;
}

View File

@@ -25,7 +25,8 @@ import { CodeBuilderFactory } from '@/application/Context/State/Code/Generation/
import SizeObserver from '@/presentation/components/Shared/SizeObserver.vue'; import SizeObserver from '@/presentation/components/Shared/SizeObserver.vue';
import { NonCollapsing } from '@/presentation/components/Scripts/View/Cards/NonCollapsingDirective'; import { NonCollapsing } from '@/presentation/components/Scripts/View/Cards/NonCollapsingDirective';
import type { ProjectDetails } from '@/domain/Project/ProjectDetails'; 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({ export default defineComponent({
components: { components: {
@@ -34,13 +35,7 @@ export default defineComponent({
directives: { directives: {
NonCollapsing, NonCollapsing,
}, },
props: { setup() {
theme: {
type: String,
default: undefined,
},
},
setup(props) {
const { onStateChange, currentState } = injectKey((keys) => keys.useCollectionState); const { onStateChange, currentState } = injectKey((keys) => keys.useCollectionState);
const { projectDetails } = injectKey((keys) => keys.useApplication); const { projectDetails } = injectKey((keys) => keys.useApplication);
const { events } = injectKey((keys) => keys.useAutoUnsubscribedEvents); const { events } = injectKey((keys) => keys.useAutoUnsubscribedEvents);
@@ -48,8 +43,8 @@ export default defineComponent({
const editorId = 'codeEditor'; const editorId = 'codeEditor';
const highlightedRange = ref(0); const highlightedRange = ref(0);
let editor: ace.Ace.Editor | undefined; let editor: CodeEditor | undefined;
let currentMarkerId: number | undefined; let currentMarker: CodeEditorStyleHandle | undefined;
onUnmounted(() => { onUnmounted(() => {
destroyEditor(); destroyEditor();
@@ -63,11 +58,10 @@ export default defineComponent({
function handleNewState(newState: IReadOnlyCategoryCollectionState) { function handleNewState(newState: IReadOnlyCategoryCollectionState) {
destroyEditor(); destroyEditor();
editor = initializeEditor( editor = initializeAceEditor({
props.theme, editorContainerElementId: editorId,
editorId, language: getLanguage(newState.collection.scripting.language),
newState.collection.scripting.language, });
);
const appCode = newState.code; const appCode = newState.code;
updateCode(appCode.current, newState.collection.scripting.language); updateCode(appCode.current, newState.collection.scripting.language);
events.unsubscribeAllAndRegister([ events.unsubscribeAllAndRegister([
@@ -77,7 +71,7 @@ export default defineComponent({
function updateCode(code: string, language: ScriptingLanguage) { function updateCode(code: string, language: ScriptingLanguage) {
const innerCode = code || getDefaultCode(language, projectDetails); const innerCode = code || getDefaultCode(language, projectDetails);
editor?.setValue(innerCode, 1); editor?.setContent(innerCode);
} }
function handleCodeChange(event: ICodeChangedEvent) { function handleCodeChange(event: ICodeChangedEvent) {
@@ -91,7 +85,7 @@ export default defineComponent({
} }
function sizeChanged() { function sizeChanged() {
editor?.resize(); editor?.updateSize();
} }
function destroyEditor() { function destroyEditor() {
@@ -100,11 +94,11 @@ export default defineComponent({
} }
function removeCurrentHighlighting() { function removeCurrentHighlighting() {
if (!currentMarkerId) { if (!currentMarker) {
return; return;
} }
editor?.session.removeMarker(currentMarkerId); currentMarker?.clearStyle();
currentMarkerId = undefined; currentMarker = undefined;
highlightedRange.value = 0; highlightedRange.value = 0;
} }
@@ -117,28 +111,15 @@ export default defineComponent({
const end = Math.max( const end = Math.max(
...positions.map((position) => position.endLine), ...positions.map((position) => position.endLine),
); );
scrollToLine(end + 2); editor?.scrollToLine(end + 2);
highlight(start, end); highlight(start, end);
} }
function highlight(startRow: number, endRow: number) { function highlight(startRow: number, endRow: number) {
const AceRange = ace.require('ace/range').Range; currentMarker = editor?.applyStyleToLineRange(startRow, endRow, 'code-area__highlight');
currentMarkerId = editor?.session.addMarker(
new AceRange(startRow, 0, endRow, 0),
'code-area__highlight',
'fullLine',
);
highlightedRange.value = endRow - startRow; 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 { return {
editorId, editorId,
highlightedRange, highlightedRange,
@@ -147,29 +128,12 @@ export default defineComponent({
}, },
}); });
function initializeEditor( function getLanguage(language: ScriptingLanguage): SupportedSyntaxLanguage {
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) {
switch (language) { switch (language) {
case ScriptingLanguage.batchfile: return 'batchfile'; case ScriptingLanguage.batchfile: return 'batchfile';
case ScriptingLanguage.shellscript: return 'sh'; case ScriptingLanguage.shellscript: return 'shellscript';
default: default:
throw new Error('unknown language'); throw new Error(`Unsupported language: ${language}`);
} }
} }

View File

@@ -11,7 +11,7 @@
<TheScriptsView :current-view="currentView" /> <TheScriptsView :current-view="currentView" />
</template> </template>
<template #second> <template #second>
<TheCodeArea theme="xcode" /> <TheCodeArea />
</template> </template>
</HorizontalResizeSlider> </HorizontalResizeSlider>
</div> </div>