Skip to content

Commit

Permalink
Track editor state changes in more clear way
Browse files Browse the repository at this point in the history
  • Loading branch information
xingrz committed Mar 26, 2024
1 parent f6527fb commit 471e1a6
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 75 deletions.
2 changes: 1 addition & 1 deletion src/components/BSMap/BSRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const texts = computed(() => {
function isFocused(row: number, offset: number, length: number): boolean {
const { selection } = editorStore;
return (
selection != null &&
typeof selection != 'undefined' &&
selection.row == row &&
selection.offset >= offset &&
selection.offset <= offset + length
Expand Down
105 changes: 32 additions & 73 deletions src/components/Editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,20 @@
</template>

<script lang="ts" setup>
import { defineEmits, defineProps, onMounted, toRefs, watch, ref, useCssModule } from 'vue';
import {
defineProps,
defineModel,
onBeforeUnmount,
onMounted,
watch,
ref,
useCssModule,
} from 'vue';
import brace, {
Range,
Position,
IEditSession,
Editor as IEditor,
VirtualRenderer,
} from 'brace';
import 'brace/theme/tomorrow';
import 'brace/ext/language_tools';
Expand All @@ -24,92 +30,45 @@ import ISelection from '@/types/selection';
import IIcon from '@/types/icon';
import useClientSize from '@/composables/useClientSize';
interface IRenderer extends VirtualRenderer {
scrollTop: number;
scrollSelectionIntoView(): void;
}
import bindEditorValue from '@/composables/bindEditorValue';
import bindEditorSelection from '@/composables/bindEditorSelection';
import bindEditorScroll from '@/composables/bindEditorScroll';
const props = defineProps<{
content: string;
selection: ISelection | null;
scroll: number;
icons: Record<string, IIcon | null>;
size: number;
}>();
const emit = defineEmits<{
(e: 'update:content', content: string): void;
(e: 'update:selection', selection: ISelection | null): void;
(e: 'update:scroll', scroll: number): void;
}>();
const holder = ref<HTMLElement>();
const editor = ref<IEditor>();
const { scroll, selection } = toRefs(props);
const content = defineModel<string>('content');
bindEditorValue(editor, content);
let editor: IEditor | undefined;
let renderer: IRenderer | undefined;
const selection = defineModel<ISelection>('selection');
bindEditorSelection(editor, selection);
watch(scroll, (scroll) => renderer?.scrollToY(scroll));
const scroll = defineModel<number>('scroll');
bindEditorScroll(editor, scroll);
function applySelection(row: number, offset: number, length: number) {
if (renderer && editor) {
const { selection } = editor.getSession();
selection.setRange({
start: { row: row, column: offset },
end: { row: row, column: offset + length },
} as Range, false);
renderer.scrollToX(0);
renderer.scrollSelectionIntoView();
editor.focus();
}
}
onMounted(() => {
editor.value = brace.edit(holder.value!);
editor.value.$blockScrolling = Infinity;
const session = editor.value.getSession();
session.setMode('ace/mode/rdt');
watch(selection, (selection) => {
if (selection && selection.from != 'editor') {
applySelection(selection.row, selection.offset, selection.length);
}
editor.value.setTheme('ace/theme/tomorrow');
editor.value.setOption('enableLiveAutocompletion', [{ getCompletions, getDocTooltip }]);
});
const holder = ref<HTMLElement | undefined>();
onMounted(() => {
if (holder.value) {
editor = brace.edit(holder.value);
editor.$blockScrolling = Infinity;
renderer = editor.renderer as IRenderer;
const session = editor.getSession();
session.setMode('ace/mode/rdt');
session.on('changeScrollTop', () => {
if (renderer) emit('update:scroll', renderer.scrollTop);
});
editor.setTheme('ace/theme/tomorrow');
editor.setValue(props.content);
editor.setOption('enableLiveAutocompletion', [{ getCompletions, getDocTooltip }]);
editor.on('change', () => {
if (editor) emit('update:content', editor.getValue());
});
editor.selection.on('changeCursor', () => {
if (editor) {
const { start } = editor.getSelection().getRange();
emit('update:selection', {
row: start.row,
offset: start.column,
length: 0,
from: 'editor',
});
}
});
applySelection(0, 0, 0);
}
onBeforeUnmount(() => {
editor.value?.destroy();
editor.value = undefined;
});
const holderSize = useClientSize(holder);
watch(holderSize, () => editor?.resize());
watch(holderSize, () => editor.value?.resize());
function getCompletions(
_editor: IEditor,
Expand Down
25 changes: 25 additions & 0 deletions src/composables/bindEditorScroll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { watch, type Ref } from 'vue';
import type { Editor } from 'brace';

import onRefAssigned from './onRefAssigned';

export default function bindEditorScroll(editor: Ref<Editor | undefined>, scroll: Ref<number | undefined>): void {
onRefAssigned(editor, (value) => {
const session = value.getSession();
session.on('changeScrollTop', () => {
const scrollTop = session.getScrollTop();
if (scroll.value != scrollTop) {
scroll.value = scrollTop;
}
});
});

watch(scroll, (value) => {
if (typeof value != 'undefined' && editor.value) {
const session = editor.value.getSession();
if (session.getScrollTop() !== value) {
session.setScrollTop(value);
}
}
});
}
57 changes: 57 additions & 0 deletions src/composables/bindEditorSelection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { watch, type Ref } from 'vue';
import type { Editor, Range, VirtualRenderer } from 'brace';

import onRefAssigned from './onRefAssigned';

import ISelection from '@/types/selection';

interface Renderer extends VirtualRenderer {
scrollTop: number;
scrollSelectionIntoView(): void;
}

export default function bindEditorSelection(editorRef: Ref<Editor | undefined>, selection: Ref<ISelection | undefined>): void {
onRefAssigned(editorRef, (editor) => {
applyRange(editor, toRange({ row: 0, offset: 0, length: 0 }));

editor.selection.on('changeCursor', () => {
const range = editor.getSelection().getRange();
selection.value = toSelection(range, 'editor');
});
});

watch(selection, (selection) => {
const editor = editorRef.value;
// Do not respond to selection changes dispatched from the editor
// This can't be done with a comparation between two selection objects,
// since the object generated from the editor is different, especially on
// multi-line selections or deletions.
if (editor && selection && selection.from != 'editor') {
applyRange(editor, toRange(selection));
}
});
}

function toRange({ row, offset, length }: Omit<ISelection, 'from'>): Range {
return {
start: { row: row, column: offset },
end: { row: row, column: offset + length },
} as Range;
}

function applyRange(editor: Editor, range: Range) {
const renderer = editor.renderer as Renderer;
editor.selection.setRange(range, false);
renderer.scrollToX(0);
renderer.scrollSelectionIntoView();
editor.focus();
}

function toSelection(range: Range, from: ISelection['from']): ISelection {
return {
row: range.start.row,
offset: range.start.column,
length: range.end.column - range.start.column,
from: from,
} as ISelection;
}
18 changes: 18 additions & 0 deletions src/composables/bindEditorValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Ref } from 'vue';
import type { Editor } from 'brace';

import onRefAssigned from './onRefAssigned';

export default function bindEditorValue(editor: Ref<Editor | undefined>, val: Ref<string | undefined>): void {
onRefAssigned(editor, (value) => {
if (val.value) {
value.setValue(val.value);
}

value.on('change', () => {
val.value = value.getValue();
});
});

// only one way binding
}
9 changes: 9 additions & 0 deletions src/composables/onRefAssigned.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { type WatchSource, type WatchStopHandle, watch } from 'vue';

export default function onRefAssigned<T>(value: WatchSource<T | undefined>, callback: (newValue: T) => void): WatchStopHandle {
return watch(value, (newValue, oldValue) => {
if (typeof newValue != 'undefined' && typeof oldValue == 'undefined') {
callback(newValue);
}
});
}
9 changes: 9 additions & 0 deletions src/composables/onRefUnassigned.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { type WatchSource, type WatchStopHandle, watch } from 'vue';

export default function onRefUnassigned<T>(value: WatchSource<T | undefined>, callback: (oldValue: T) => void): WatchStopHandle {
return watch(value, (newValue, oldValue) => {
if (typeof newValue == 'undefined' && typeof oldValue != 'undefined') {
callback(oldValue);
}
});
}
2 changes: 1 addition & 1 deletion src/stores/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const useEditorStore = defineStore('editor', () => {
const width = ref(parseInt(localStorage.getItem('width') || '') || 200);
const content = ref(localStorage.getItem('content') || '');
const scroll = ref(0);
const selection = ref<ISelection | null>(null);
const selection = ref<ISelection>();

watch(size, (value) => {
debouncedSetItem('size', String(value));
Expand Down

0 comments on commit 471e1a6

Please sign in to comment.