Merge pull request #2849 from beekeeper-studio/feat/editable-json-viewer

Make JSON Row Viewer Editable
This commit is contained in:
Mohammad Azmi
2025-04-02 20:43:54 +07:00
committed by GitHub
14 changed files with 477 additions and 32 deletions

View File

@@ -70,6 +70,8 @@
"ini": "^5.0.0",
"javascript-time-ago": "^2.0.8",
"jquery": "^3.5.0",
"json-pointer": "^0.6.2",
"json-source-map": "^0.6.1",
"jsonc-parser": "^3.3.1",
"knex": "^2.4.1",
"knex-firebird-dialect": "1.4.6",

View File

@@ -69,7 +69,7 @@
}
}
.CodeMirror.text-editor {
.text-editor {
width: 100%;
height: auto;
padding-left: 1rem;

View File

@@ -0,0 +1,13 @@
.CodeMirror.text-editor.bks-partial-read-only {
.CodeMirror-lines {
cursor: default;
}
.bks-editable-marker {
cursor: text;
}
.bks-editable-marker:hover, .bks-editable-marker-active {
background-color: rgba($theme-base, 0.08);
}
}

View File

@@ -1,3 +1,5 @@
@import "./text-editor-plugins/partial-read-only.scss";
.text-editor {
&.remove-json-root-brackets .CodeMirror-code {
& > div:first-child, & > div:last-child {
@@ -35,4 +37,10 @@
.CodeMirror-guttermaker-subtle {
color: rgba($theme-base, 0.25);
}
.bks-error-marker {
text-decoration-line: underline;
text-decoration-style: wavy;
text-decoration-color: rgba($brand-danger, 0.6);
}
}

View File

@@ -60,6 +60,7 @@ export enum AppEvent {
switchUserKeymap = 'switchUserKeymap',
updateJsonViewerSidebar = 'updateJsonViewerSidebar',
jsonViewerSidebarExpandPath = 'jsonViewerSidebarExpandPath',
jsonViewerSidebarValueChange = 'jsonViewerSidebarValueChange',
switchingTab = 'switchingTab',
switchedTab = 'switchedTab',
}

View File

@@ -43,11 +43,15 @@ import {
import { AppEvent } from "@/common/AppEvent";
import { keymapTypes } from "@/lib/db/types"
import { EditorMarker, LineGutter } from "@/lib/editor/utils";
import { TextEditorPlugin } from "@/lib/editor/plugins/TextEditorPlugin";
import rawLog from '@bksLogger'
interface InitializeOptions {
userKeymap?: typeof keymapTypes[number]['value']
}
const log = rawLog.scope('TextEditor')
export default {
props: [
"value",
@@ -87,6 +91,7 @@ export default {
activeLineGutters: [],
wasEditorFocused: false,
firstInitialization: true,
initializedPlugins: [],
};
},
computed: {
@@ -188,6 +193,12 @@ export default {
unfoldAll() {
CodeMirror.commands.unfoldAll(this.editor)
},
plugins() {
this.destroyPlugins();
if (this.editor) {
this.initializePlugins(this.editor);
}
},
},
methods: {
focusEditor() {
@@ -342,11 +353,7 @@ export default {
}
}
if (this.plugins) {
this.plugins.forEach((plugin: (cm: CodeMirror.Editor) => void) => {
plugin(cm);
});
}
this.initializePlugins(cm)
if (this.firstInitialization && this.focus) {
cm.focus();
@@ -374,7 +381,8 @@ export default {
let markInstance: TextMarker;
if (marker.type === "error") {
markInstance = this.editor.markText(marker.from, marker.to, {
className: "error",
className: "bks-error-marker",
attributes: { title: marker.message },
});
} else if (marker.type === "highlight") {
markInstance = this.editor.markText(marker.from, marker.to, {
@@ -436,13 +444,39 @@ export default {
this.activeLineGutters.push(lineGutter)
})
},
initializePlugins(editor: CodeMirror.Editor) {
this.plugins?.forEach((plugin: ((editor: CodeMirror.Editor) => Function) | TextEditorPlugin) => {
try {
if (typeof plugin === "function") {
const destroy = plugin(editor);
this.initializedPlugins.push({ destroy });
} else {
plugin.initialize(editor);
this.initializedPlugins.push(plugin);
}
} catch (e) {
log.error("Error initializing plugin", e)
}
});
},
destroyEditor() {
this.destroyPlugins();
if (this.editor) {
this.editor
.getWrapperElement()
.parentNode.removeChild(this.editor.getWrapperElement());
}
},
destroyPlugins() {
this.initializedPlugins.forEach((plugin: TextEditorPlugin) => {
try {
plugin.destroy()
} catch (e) {
log.error("Error destroying plugin", e)
}
})
this.initializedPlugins = []
},
showContextMenu(event) {
const hasSelectedText = this.editor.getSelection();
const selectionDepClass = hasSelectedText ? "" : "disabled";
@@ -543,9 +577,18 @@ export default {
menu.options = menu.options.filter((option) => !option.write);
}
const customOptions = this.contextMenuOptions
? this.contextMenuOptions(event, menu.options)
: undefined;
let customOptions: typeof menu.options | false | undefined;
for (const plugin of this.initializedPlugins) {
if (plugin.beforeOpeningContextMenu) {
customOptions = plugin.beforeOpeningContextMenu(event, menu.options)
}
}
// FIXME remove this in favor of plugin
if (this.contextMenuOptions) {
customOptions = this.contextMenuOptions(event, customOptions || menu.options);
}
if (customOptions === false) {
return;

View File

@@ -45,7 +45,6 @@
</div>
</div>
<text-editor
:read-only="true"
:fold-gutter="true"
:fold-all="foldAll"
:unfold-all="unfoldAll"
@@ -57,6 +56,7 @@
:line-wrapping="wrapText"
:line-gutters="lineGutters"
:line-numbers="false"
:extra-keybindings="disableReplaceKeybindings"
/>
<div class="empty-state" v-show="empty">
Open a table to view its data
@@ -86,17 +86,20 @@ import {
import { mapGetters } from "vuex";
import { EditorMarker, LineGutter } from "@/lib/editor/utils";
import { persistJsonFold } from "@/lib/editor/plugins/persistJsonFold";
import PartialReadOnlyPlugin from "@/lib/editor/plugins/PartialReadOnlyPlugin";
import JsonViewerUpsell from '@/components/upsell/JsonViewerSidebarUpsell.vue'
import rawLog from "@bksLogger";
import _ from "lodash";
import globals from '@/common/globals'
import JsonSourceMap from "json-source-map";
import JsonPointer from "json-pointer";
import { typedArrayToString } from '@/common/utils'
const log = rawLog.scope("json-viewer");
export default Vue.extend({
components: { TextEditor, JsonViewerUpsell },
props: ["value", "hidden", "expandablePaths", "dataId", "title", "reinitialize", "signs", "binaryEncoding"],
props: ["value", "hidden", "expandablePaths", "editablePaths", "dataId", "title", "reinitialize", "signs", "binaryEncoding"],
data() {
return {
reinitializeTextEditor: 0,
@@ -104,6 +107,11 @@ export default Vue.extend({
foldAll: 0,
unfoldAll: 0,
restoredTruncatedPaths: [],
editableRangeErrors: [],
disableReplaceKeybindings: {
[this.cmCtrlOrCmd("R")]: () => false,
[this.cmCtrlOrCmd("Shift-R")]: () => false,
},
wrapText: false,
};
},
@@ -136,14 +144,7 @@ export default Vue.extend({
return { name: "javascript", json: true };
},
text() {
if (this.empty) {
return "";
}
if (this.filter) {
const filtered = deepFilterObjectProps(this.processedValue, this.filter);
return JSON.stringify(filtered, this.replacer, 2);
}
return JSON.stringify(this.processedValue, this.replacer, 2);
return this.sourceMap.json
},
debouncedFilter: {
get() {
@@ -153,6 +154,18 @@ export default Vue.extend({
this.filter = value;
}, 500),
},
sourceMap() {
return JsonSourceMap.stringify(this.filteredValue, null, 2);
},
filteredValue() {
if (this.empty) {
return {}
}
if (!this.filter) {
return this.processedValue
}
return deepFilterObjectProps(this.processedValue, this.filter);
},
processedValue() {
const clonedValue = _.cloneDeep(this.value)
eachPaths(clonedValue, (path, value) => {
@@ -225,6 +238,14 @@ export default Vue.extend({
log.warn(e);
}
})
_.forEach(this.editableRangeErrors, ({ error, from, to }) => {
markers.push({
type: "error",
from,
to,
message: error.message,
});
})
return markers;
},
lines() {
@@ -243,6 +264,33 @@ export default Vue.extend({
})
return lineGutters;
},
editableRanges() {
const editablePaths = this.editablePaths
if (_.isEmpty(editablePaths)) {
return []
}
const ranges = []
editablePaths.forEach((path: string) => {
const pointer = JsonPointer.compile(path.split("."))
const position = this.sourceMap.pointers[pointer]
if (!position) {
log.warn(`Unable to find editable path \`${path}\` in value object.`)
return
}
ranges.push({
id: path,
from: { line: position.value.line, ch: position.value.column },
to: { line: position.valueEnd.line, ch: position.valueEnd.column },
})
})
return ranges
},
menuOptions() {
return [
{
@@ -281,7 +329,10 @@ export default Vue.extend({
]
},
textEditorPlugins() {
return [persistJsonFold]
return [
persistJsonFold,
new PartialReadOnlyPlugin(this.editableRanges, this.handleEditableRangeChange),
]
},
...mapGetters(["expandFKDetailsByDefault"]),
},
@@ -295,6 +346,15 @@ export default Vue.extend({
expandPath(path: ExpandablePath) {
this.$emit("expandPath", path);
},
handleEditableRangeChange: _.debounce(function (range, value) {
this.editableRangeErrors = []
try {
const parsed = JSON.parse(value)
this.$emit("bks-json-value-change", {key: range.id, value: parsed});
} catch (error) {
this.editableRangeErrors.push({ id: range.id, error, from: range.from, to: range.to })
}
}, 250),
},
});
</script>

View File

@@ -7,6 +7,8 @@
:reinitialize="reinitializeJsonViewer"
:signs="signs"
:binary-encoding="$bksConfig.ui.general.binaryEncoding"
:editable-paths="editablePaths"
@bks-json-value-change="handleJsonValueChange"
@expandPath="handleExpandPath"
/>
</template>
@@ -30,6 +32,7 @@ export default Vue.extend({
return {
value: {},
expandablePaths: [],
editablePaths: [],
signs: {},
};
},
@@ -54,7 +57,10 @@ export default Vue.extend({
handleExpandPath(expandablePaths: string[]) {
this.trigger(AppEvent.jsonViewerSidebarExpandPath, expandablePaths)
},
update(options: { value: Record<string, unknown>; expandablePaths: string[], signs: Record<string, LineGutter['type']>}) {
handleJsonValueChange(detail: { key: string; value: unknown }) {
this.trigger(AppEvent.jsonViewerSidebarValueChange, detail)
},
update(options: { value: Record<string, unknown>; expandablePaths: string[], signs: Record<string, LineGutter['type']>, editablePaths: string[] }) {
this.value = options.value
this.expandablePaths = options.expandablePaths
this.signs = options.signs

View File

@@ -368,12 +368,13 @@ export default Vue.extend({
initialized: false,
internalColumnPrefix: "__beekeeper_internal_",
internalIndexColumn: "__beekeeper_internal_index",
selectedRowIndex: null,
/** This is true when we switch to minimal mode while TableTable is not active */
enabledMinimalModeWhileInactive: false,
selectedRow: null,
selectedRowIndex: null,
selectedRowPosition: null,
selectedRowData: {},
expandablePaths: {},
};
@@ -568,12 +569,24 @@ export default Vue.extend({
selectedRowDataSigns() {
const signs = {}
for (const pendingUpdate of this.pendingChanges.updates) {
if (pendingUpdate.rowIndex === this.selectedRowIndex) {
if (pendingUpdate.rowIndex === this.selectedRowPosition) {
signs[pendingUpdate.column] = "changed"
}
}
return signs
},
editablePaths() {
if (!this.table.columns) return []
const paths = []
for (const column of this.table.columns) {
if(this.isPrimaryKey(column.columnName) || this.isForeignKey(column.columnName) || this.isGeneratedColumn(column.columnName)) {
continue
}
paths.push(column.columnName)
}
return paths
},
},
watch: {
@@ -906,7 +919,11 @@ export default Vue.extend({
this.$root.$emit(AppEvent.closeTab)
},
isPrimaryKey(column) {
return this.primaryKeys.includes(column);
return this.primaryKeys?.includes(column);
},
isForeignKey(column: string) {
const keyDatas: any[] = Object.entries(this.tableKeys).filter((entry) => entry[0] === column);
return keyDatas && keyDatas.length > 0
},
isGeneratedColumn(columnName: string) {
const column: ExtendedTableColumn = this.table.columns.find((col: ExtendedTableColumn) => col.columnName === columnName);
@@ -1226,7 +1243,7 @@ export default Vue.extend({
}
// reflect changes in the detail view
if (this.indexRowOf(cell.getRow()) === this.selectedRowIndex) {
if (this.positionRowOf(cell.getRow()) === this.selectedRowIndex) {
cell.getRow().invalidateForeignCache(cell.getField())
this.updateJsonViewer()
}
@@ -1274,12 +1291,13 @@ export default Vue.extend({
oldValue: cell.getOldValue(),
cell: cell,
value: cell.getValue(0),
rowIndex: this.indexRowOf(cell.getRow())
rowIndex: this.positionRowOf(cell.getRow())
}
// remove existing pending updates with identical pKey-column combo
let pendingUpdates = _.reject(this.pendingChanges.updates, { 'key': payload.key })
pendingUpdates.push(payload)
this.$set(this.pendingChanges, 'updates', pendingUpdates)
this.updateDetailView()
}
},
cloneSelection(range?: RangeComponent) {
@@ -1709,7 +1727,7 @@ export default Vue.extend({
this.tabulator.setPage(page)
if (!this.active) this.forceRedraw = true
},
indexRowOf(row: RowComponent) {
positionRowOf(row: RowComponent) {
return (this.limit * (this.page - 1)) + (row.getPosition() || 0)
},
updateJsonViewer(options: { range?: RangeComponent } = {}) {
@@ -1717,15 +1735,16 @@ export default Vue.extend({
const row = range.getRows()[0]
if (!row) {
this.selectedRow = null
this.selectedRowIndex = null
this.selectedRowPosition = null
this.selectedRowData = {}
return
}
const position = this.indexRowOf(row)
const position = this.positionRowOf(row)
const data = row.getData("withForeignData")
const cachedExpandablePaths = row.getExpandablePaths()
this.selectedRow = row
this.selectedRowIndex = position
this.selectedRowPosition = position
this.selectedRowIndex = this.primaryKeys?.map((key: string) => data[key]).join(',');
this.selectedRowData = this.$bks.cleanData(data, this.tableColumns)
this.expandablePaths = this.rawTableKeys
.filter((key) => !row.hasForeignData([key.fromColumn]))
@@ -1739,6 +1758,7 @@ export default Vue.extend({
value: this.selectedRowData,
expandablePaths: this.expandablePaths,
signs: this.selectedRowDataSigns,
editablePaths: this.editablePaths,
}
this.trigger(AppEvent.updateJsonViewerSidebar, updatedData)
@@ -1843,6 +1863,7 @@ export default Vue.extend({
value: this.selectedRowData,
expandablePaths: this.expandablePaths,
signs: this.selectedRowDataSigns,
editablePaths: this.editablePaths,
}
this.trigger(AppEvent.updateJsonViewerSidebar, data)
@@ -1862,6 +1883,7 @@ export default Vue.extend({
value: this.selectedRowData,
expandablePaths: this.expandablePaths,
signs: this.selectedRowDataSigns,
editablePaths: this.editablePaths,
}
this.trigger(AppEvent.updateJsonViewerSidebar, data)
this.registerHandlers([
@@ -1869,6 +1891,10 @@ export default Vue.extend({
event: AppEvent.jsonViewerSidebarExpandPath,
handler: this.expandForeignKey,
},
{
event: AppEvent.jsonViewerSidebarValueChange,
handler: this.handleJsonValueChange,
},
])
},
handleTabInactive() {
@@ -1877,8 +1903,15 @@ export default Vue.extend({
event: AppEvent.jsonViewerSidebarExpandPath,
handler: this.expandForeignKey,
},
{
event: AppEvent.jsonViewerSidebarValueChange,
handler: this.handleJsonValueChange,
},
])
},
handleJsonValueChange({key, value}) {
this.selectedRow?.getCell(key).setValue(value)
},
debouncedSaveTab: _.debounce(function(tab) {
this.$store.dispatch('tabs/save', tab)
}, 300),

View File

@@ -0,0 +1,229 @@
/**
* This plugin sets the whole editor to be read-only except for some defined
* ranges. **/
import _ from "lodash";
import { EditorRange, isPositionWithin } from "@/lib/editor/utils";
import { MenuOption, TextEditorPlugin } from "./TextEditorPlugin";
import CodeMirror from "codemirror";
// FIXME (azmi): maybe use json-ource-map instead?
const PARTIAL_READ_ONLY_CLASSNAME = "bks-partial-read-only";
const EDITABLE_MARKER_CLASSNAME = "bks-editable-marker";
const EDITABLE_MARKER_ACTIVE_CLASSNAME = "bks-editable-marker-active";
const EDITABLE_MARKER_ATTR_ID = "data-bks-editable-id";
export default class PartialReadOnlyPlugin extends TextEditorPlugin {
name = "partialReadOnly";
rangeMap = new Map<
string,
{
range: EditorRange;
marker: CodeMirror.TextMarker;
}
>();
activeMarkerId?: string;
editor: CodeMirror.Editor;
constructor(
private ranges: EditorRange[],
private onEditableRangeChange?: (range: EditorRange, value: string) => void
) {
super();
this.handleBeforeChange = this.handleBeforeChange.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleCursorActivity = this.handleCursorActivity.bind(this);
this.handleMouseOver = this.handleMouseOver.bind(this);
this.handleMouseOut = this.handleMouseOut.bind(this);
}
initialize(editor: CodeMirror.Editor) {
if (editor.getOption("readOnly")) {
throw new Error(
"PartialReadOnlyPlugin requires `readOnly` to be disabled."
);
}
this.editor = editor;
this.editor.getWrapperElement().classList.add(PARTIAL_READ_ONLY_CLASSNAME);
this.ranges.forEach((range) => {
const marker = editor.markText(range.from, range.to, {
className: EDITABLE_MARKER_CLASSNAME,
inclusiveLeft: true, // text inserted on the left will be included
inclusiveRight: true, // text inserted on the right will be included
clearWhenEmpty: false,
// @ts-expect-error not fully typed
attributes: { [EDITABLE_MARKER_ATTR_ID]: range.id },
});
this.rangeMap.set(range.id, {
range,
marker,
});
});
this.editor.on("beforeChange", this.handleBeforeChange);
this.editor.on("change", this.handleChange);
this.editor.on("cursorActivity", this.handleCursorActivity);
this.setupMarkerHoverHandlers();
}
destroy(): void {
this.rangeMap.forEach((range) => range.marker.clear());
if (this.editor) {
this.editor.off("beforeChange", this.handleBeforeChange);
this.editor.off("change", this.handleChange);
this.editor.off("cursorActivity", this.handleCursorActivity);
this.removeMarkerHoverHandlers();
this.editor
.getWrapperElement()
.classList.remove(PARTIAL_READ_ONLY_CLASSNAME);
}
}
beforeOpeningContextMenu(_event: MouseEvent, menuOptions: MenuOption[]) {
return menuOptions.filter((option) => !option.write);
}
private setupMarkerHoverHandlers(): void {
const wrapper = this.editor.getWrapperElement();
wrapper.addEventListener("mouseover", this.handleMouseOver);
wrapper.addEventListener("mouseout", this.handleMouseOut);
}
private removeMarkerHoverHandlers(): void {
const wrapper = this.editor.getWrapperElement();
wrapper.removeEventListener("mouseover", this.handleMouseOver);
wrapper.removeEventListener("mouseout", this.handleMouseOut);
}
private handleBeforeChange(
cm: CodeMirror.Editor,
changeObj: CodeMirror.EditorChangeCancellable
) {
if (changeObj.origin !== "setValue") {
let markers = cm.findMarks(changeObj.from, changeObj.to);
if (markers.length === 0) {
markers = cm.findMarksAt(changeObj.from);
}
for (const marker of markers) {
if (
this.getMarkerId(marker) &&
isPositionWithin(changeObj, marker.find())
) {
return;
}
}
changeObj.cancel();
}
}
private handleChange(
cm: CodeMirror.Editor,
changeObj: CodeMirror.EditorChangeLinkedList
) {
// Skip the whole process if there is no callback
if (!this.onEditableRangeChange) {
return;
}
if (changeObj.origin !== "setValue") {
const markers = cm.findMarksAt(changeObj.from);
for (const marker of markers) {
const id = this.getMarkerId(marker)
if (!id) {
continue;
}
const markerPos = marker.find();
const range: EditorRange = {
id,
from: markerPos.from,
to: markerPos.to,
}
const value = this.getMarkerValue(marker);
this.onEditableRangeChange(range, value);
}
}
}
private handleCursorActivity(cm: CodeMirror.Editor) {
const cursor = cm.getCursor();
const markers = cm.findMarksAt(cursor);
const activeMarker = this.findActiveMarker(markers);
let activeMarkerId: string;
if (activeMarker) {
activeMarkerId = this.getMarkerId(activeMarker);
setTimeout(() => {
this.highlightMarker(activeMarkerId);
});
}
if (this.activeMarkerId && this.activeMarkerId !== activeMarkerId) {
this.unHighlightMarker(this.activeMarkerId);
}
this.activeMarkerId = activeMarkerId;
}
private handleMouseOver(e: MouseEvent) {
const target = e.target as HTMLElement;
if (target.classList.contains(EDITABLE_MARKER_CLASSNAME)) {
const markerId = target.getAttribute(EDITABLE_MARKER_ATTR_ID);
if (markerId) {
this.highlightMarker(markerId);
}
}
}
private handleMouseOut(e: MouseEvent) {
const target = e.target as HTMLElement;
if (target.classList.contains(EDITABLE_MARKER_CLASSNAME)) {
const markerId = target.getAttribute(EDITABLE_MARKER_ATTR_ID);
if (markerId && markerId !== this.activeMarkerId) {
this.unHighlightMarker(markerId);
}
}
}
highlightMarker(markerId: string) {
const markerEls = this.editor
.getWrapperElement()
.querySelectorAll(`[${EDITABLE_MARKER_ATTR_ID}="${markerId}"]`);
markerEls.forEach((el) => {
el.classList.toggle(EDITABLE_MARKER_ACTIVE_CLASSNAME, true);
});
}
unHighlightMarker(markerId: string) {
const markerEls = this.editor
.getWrapperElement()
.querySelectorAll(`[${EDITABLE_MARKER_ATTR_ID}="${markerId}"]`);
markerEls.forEach((el) => {
el.classList.toggle(EDITABLE_MARKER_ACTIVE_CLASSNAME, false);
});
}
findActiveMarker(markers: CodeMirror.TextMarker[]) {
return markers.find((marker) => this.getMarkerId(marker));
}
getMarkerValue(marker: CodeMirror.TextMarker) {
const { from, to } = marker.find();
return this.editor.getRange(from, to);
}
getMarkerId(marker: CodeMirror.TextMarker) {
// @ts-expect-error not fully typed
return marker.attributes?.[EDITABLE_MARKER_ATTR_ID];
}
}

View File

@@ -0,0 +1,16 @@
export interface MenuOption {
name: string;
handler: Function;
shortcut: string;
write: boolean;
}
export abstract class TextEditorPlugin {
abstract name: string;
abstract initialize(editor: CodeMirror.Editor): void;
abstract destroy(): void;
beforeOpeningContextMenu?(
event: MouseEvent,
menuOptions: MenuOption[]
): void | false | MenuOption[];
}

View File

@@ -4,6 +4,7 @@
**/
import _ from "lodash";
// FIXME (azmi): maybe use json-source-map instead?
import { findKeyPosition } from "@/lib/data/jsonViewer";
import { getLocation, JSONPath } from "jsonc-parser";
import CodeMirror from "codemirror";

View File

@@ -1,9 +1,14 @@
import * as CodeMirrorPlugins from "@/lib/editor/CodeMirrorPlugins";
import { TableOrView } from "../db/models";
import CodeMirror from "codemirror";
export interface EditorMarker {
export interface EditorRange {
id?: string;
from: { line: number; ch: number };
to: { line: number; ch: number };
}
export interface EditorMarker extends EditorRange {
message?: string;
element?: HTMLElement;
onClick?: (event: MouseEvent) => void;
@@ -15,6 +20,17 @@ export interface LineGutter {
type: "changed";
}
/** Checks if `target` is within `container` */
export function isPositionWithin(
target: { from: CodeMirror.Position; to: CodeMirror.Position },
container: { from: CodeMirror.Position; to: CodeMirror.Position }
) {
return (
CodeMirror.cmpPos(target.from, container.from) >= 0 &&
CodeMirror.cmpPos(target.to, container.to) <= 0
);
}
export const plugins = {
autoquote: CodeMirrorPlugins.registerAutoquote,
autoComplete: CodeMirrorPlugins.registerAutoComplete,

View File

@@ -7407,6 +7407,11 @@ follow-redirects@^1.15.6:
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
foreach@^2.0.4:
version "2.0.6"
resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.6.tgz#87bcc8a1a0e74000ff2bf9802110708cfb02eb6e"
integrity sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==
foreground-child@^3.1.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77"
@@ -9008,11 +9013,23 @@ json-parse-even-better-errors@^2.3.0:
resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
json-pointer@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/json-pointer/-/json-pointer-0.6.2.tgz#f97bd7550be5e9ea901f8c9264c9d436a22a93cd"
integrity sha512-vLWcKbOaXlO+jvRy4qNd+TI1QUPZzfJj1tpJ3vAXDych5XJf93ftpUKe5pKCrzyIIwgBJcOcCVRUfqQP25afBw==
dependencies:
foreach "^2.0.4"
json-schema-traverse@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
json-source-map@^0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/json-source-map/-/json-source-map-0.6.1.tgz#e0b1f6f4ce13a9ad57e2ae165a24d06e62c79a0f"
integrity sha512-1QoztHPsMQqhDq0hlXY5ZqcEdUzxQEIxgFkKl4WUp2pgShObl+9ovi4kRh2TfvAfxAoHOJ9vIMEqk3k4iex7tg==
json-stable-stringify-without-jsonify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"