diff --git a/src/presentation/components/Code/Ace/AceCodeEditorFactory.ts b/src/presentation/components/Code/Ace/AceCodeEditorFactory.ts
new file mode 100644
index 000000000..b18fc6ecc
--- /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 769785824..155b1ddf2 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 000000000..14ed89930
--- /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 93edd8845..658696907 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 0a88456ea..01411f6e2 100644
--- a/src/presentation/components/Scripts/TheScriptArea.vue
+++ b/src/presentation/components/Scripts/TheScriptArea.vue
@@ -11,7 +11,7 @@
-
+