Skip to content

Commit

Permalink
Hide code highlight and cursor until interaction
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
undergroundwires committed Oct 10, 2024
1 parent 2f31bc7 commit 6aaa843
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 57 deletions.
91 changes: 91 additions & 0 deletions src/presentation/components/Code/Ace/AceCodeEditorFactory.ts
Original file line number Diff line number Diff line change
@@ -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 {
updateContent: (code) => editor.setValue(code, 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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
30 changes: 30 additions & 0 deletions src/presentation/components/Code/CodeEditorFactory.ts
Original file line number Diff line number Diff line change
@@ -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;
updateContent(code: string): void;
scrollToLine(lineNumber: number): void;
updateSize(): void;
applyStyleToLineRange(
startLineNumber: number,
endLineNumber: number,
className: string,
): CodeEditorStyleHandle;
}

export interface CodeEditorStyleHandle {
clearStyle(): void;
}
74 changes: 19 additions & 55 deletions src/presentation/components/Code/TheCodeArea.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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 { SyntaxHighlightingLanguage, CodeEditor, CodeEditorStyleHandle } from './CodeEditorFactory';

Check failure on line 29 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (ubuntu, development)

Module '"./CodeEditorFactory"' has no exported member 'SyntaxHighlightingLanguage'.

Check failure on line 29 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (ubuntu, production)

Module '"./CodeEditorFactory"' has no exported member 'SyntaxHighlightingLanguage'.

Check failure on line 29 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (ubuntu, test)

Module '"./CodeEditorFactory"' has no exported member 'SyntaxHighlightingLanguage'.

Check failure on line 29 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (macos, production)

Module '"./CodeEditorFactory"' has no exported member 'SyntaxHighlightingLanguage'.

Check failure on line 29 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (macos, development)

Module '"./CodeEditorFactory"' has no exported member 'SyntaxHighlightingLanguage'.

Check failure on line 29 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (macos, test)

Module '"./CodeEditorFactory"' has no exported member 'SyntaxHighlightingLanguage'.

Check failure on line 29 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (windows, production)

Module '"./CodeEditorFactory"' has no exported member 'SyntaxHighlightingLanguage'.

Check failure on line 29 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (windows, development)

Module '"./CodeEditorFactory"' has no exported member 'SyntaxHighlightingLanguage'.

Check failure on line 29 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (windows, test)

Module '"./CodeEditorFactory"' has no exported member 'SyntaxHighlightingLanguage'.
export default defineComponent({
components: {
Expand All @@ -34,22 +35,16 @@ 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);
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();
Expand All @@ -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([
Expand All @@ -77,7 +71,7 @@ export default defineComponent({
function updateCode(code: string, language: ScriptingLanguage) {
const innerCode = code || getDefaultCode(language, projectDetails);
editor?.setValue(innerCode, 1);
editor?.setCode(innerCode);

Check failure on line 74 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (ubuntu, development)

Property 'setCode' does not exist on type 'CodeEditor'.

Check failure on line 74 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (ubuntu, production)

Property 'setCode' does not exist on type 'CodeEditor'.

Check failure on line 74 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (ubuntu, test)

Property 'setCode' does not exist on type 'CodeEditor'.

Check failure on line 74 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (macos, production)

Property 'setCode' does not exist on type 'CodeEditor'.

Check failure on line 74 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (macos, development)

Property 'setCode' does not exist on type 'CodeEditor'.

Check failure on line 74 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (macos, test)

Property 'setCode' does not exist on type 'CodeEditor'.

Check failure on line 74 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (windows, production)

Property 'setCode' does not exist on type 'CodeEditor'.

Check failure on line 74 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (windows, development)

Property 'setCode' does not exist on type 'CodeEditor'.

Check failure on line 74 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (windows, test)

Property 'setCode' does not exist on type 'CodeEditor'.
}
function handleCodeChange(event: ICodeChangedEvent) {
Expand All @@ -91,7 +85,7 @@ export default defineComponent({
}
function sizeChanged() {
editor?.resize();
editor?.updateSize();
}
function destroyEditor() {
Expand All @@ -100,11 +94,11 @@ export default defineComponent({
}
function removeCurrentHighlighting() {
if (!currentMarkerId) {
if (!currentMarker) {
return;
}
editor?.session.removeMarker(currentMarkerId);
currentMarkerId = undefined;
currentMarker?.remove();

Check failure on line 100 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (ubuntu, development)

Property 'remove' does not exist on type 'CodeEditorStyleHandle'.

Check failure on line 100 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (ubuntu, production)

Property 'remove' does not exist on type 'CodeEditorStyleHandle'.

Check failure on line 100 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (ubuntu, test)

Property 'remove' does not exist on type 'CodeEditorStyleHandle'.

Check failure on line 100 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (macos, production)

Property 'remove' does not exist on type 'CodeEditorStyleHandle'.

Check failure on line 100 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (macos, development)

Property 'remove' does not exist on type 'CodeEditorStyleHandle'.

Check failure on line 100 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (macos, test)

Property 'remove' does not exist on type 'CodeEditorStyleHandle'.

Check failure on line 100 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (windows, production)

Property 'remove' does not exist on type 'CodeEditorStyleHandle'.

Check failure on line 100 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (windows, development)

Property 'remove' does not exist on type 'CodeEditorStyleHandle'.

Check failure on line 100 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (windows, test)

Property 'remove' does not exist on type 'CodeEditorStyleHandle'.
currentMarker = undefined;
highlightedRange.value = 0;
}
Expand All @@ -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?.addStyleToLines(startRow, endRow, 'code-area__highlight');

Check failure on line 119 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (ubuntu, development)

Property 'addStyleToLines' does not exist on type 'CodeEditor'.

Check failure on line 119 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (ubuntu, production)

Property 'addStyleToLines' does not exist on type 'CodeEditor'.

Check failure on line 119 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (ubuntu, test)

Property 'addStyleToLines' does not exist on type 'CodeEditor'.

Check failure on line 119 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (macos, production)

Property 'addStyleToLines' does not exist on type 'CodeEditor'.

Check failure on line 119 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (macos, development)

Property 'addStyleToLines' does not exist on type 'CodeEditor'.

Check failure on line 119 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (macos, test)

Property 'addStyleToLines' does not exist on type 'CodeEditor'.

Check failure on line 119 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (windows, production)

Property 'addStyleToLines' does not exist on type 'CodeEditor'.

Check failure on line 119 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (windows, development)

Property 'addStyleToLines' does not exist on type 'CodeEditor'.

Check failure on line 119 in src/presentation/components/Code/TheCodeArea.vue

View workflow job for this annotation

GitHub Actions / build-web (windows, test)

Property 'addStyleToLines' does not exist on type 'CodeEditor'.
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,
Expand All @@ -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): SyntaxHighlightingLanguage {
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}`);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/presentation/components/Scripts/TheScriptArea.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<TheScriptsView :current-view="currentView" />
</template>
<template #second>
<TheCodeArea theme="xcode" />
<TheCodeArea />
</template>
</HorizontalResizeSlider>
</div>
Expand Down

0 comments on commit 6aaa843

Please sign in to comment.