feat: adds a file structure sidebar in the editor (#1943)
Co-authored-by: Stefan Dej <meteyou@gmail.com>
This commit is contained in:
parent
f6f3c77d97
commit
695832a6d0
@ -31,6 +31,10 @@
|
||||
<v-icon small class="mr-1">{{ mdiHelp }}</v-icon>
|
||||
{{ $t('Editor.ConfigReference') }}
|
||||
</v-btn>
|
||||
<v-btn v-if="configFileStructure" text tile class="d-none d-md-flex" @click="showFileStructure()">
|
||||
<v-icon small class="mr-1">{{ mdiFormatListCheckbox }}</v-icon>
|
||||
{{ $t('Editor.FileStructure') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="restartServiceNameExists"
|
||||
color="primary"
|
||||
@ -48,17 +52,50 @@
|
||||
<v-icon>{{ mdiCloseThick }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card-text class="pa-0">
|
||||
<v-card-text class="pa-0 d-flex">
|
||||
<codemirror-async
|
||||
v-if="show"
|
||||
ref="editor"
|
||||
v-model="sourcecode"
|
||||
:name="filename"
|
||||
:file-extension="fileExtension" />
|
||||
:file-extension="fileExtension"
|
||||
class="codemirror"
|
||||
@lineChange="lineChanges" />
|
||||
<div v-if="fileStructureSidebar" class="d-none d-md-flex structure-sidebar">
|
||||
<v-treeview
|
||||
activatable
|
||||
dense
|
||||
:active="structureActive"
|
||||
:open="structureOpen"
|
||||
item-key="line"
|
||||
:items="configFileStructure"
|
||||
class="w-100"
|
||||
@update:active="activeChanges">
|
||||
<template #label="{ item }">
|
||||
<div
|
||||
class="cursor-pointer _structure-sidebar-item"
|
||||
:class="item.type == 'item' ? 'ͼp' : 'ͼt'">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="restartServiceName === 'klipper'" #append="{ item }">
|
||||
<v-btn
|
||||
v-if="item.type == 'section'"
|
||||
icon
|
||||
small
|
||||
plain
|
||||
color="grey darken-2"
|
||||
:href="klipperConfigReference + '#' + item.name.split(' ')[0]"
|
||||
target="_blank">
|
||||
<v-icon small class="mr-1">{{ mdiHelpCircle }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-treeview>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</panel>
|
||||
</v-dialog>
|
||||
<v-snackbar v-model="loaderBool" :timeout="-1" :value="true" fixed right bottom>
|
||||
<v-snackbar v-model="loaderBool" :timeout="-1" fixed right bottom>
|
||||
<div>
|
||||
{{ snackbarHeadline }}
|
||||
<br />
|
||||
@ -123,7 +160,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Mixins, Watch } from 'vue-property-decorator'
|
||||
import { Component, Mixins, Ref, Watch } from 'vue-property-decorator'
|
||||
import BaseMixin from '@/components/mixins/base'
|
||||
import { capitalize, formatFilesize, windowBeforeUnloadFunction } from '@/plugins/helpers'
|
||||
import Panel from '@/components/ui/Panel.vue'
|
||||
@ -139,9 +176,10 @@ import {
|
||||
mdiHelpCircle,
|
||||
mdiRestart,
|
||||
mdiUsb,
|
||||
mdiFormatListCheckbox,
|
||||
} from '@mdi/js'
|
||||
import type Codemirror from '@/components/inputs/Codemirror.vue'
|
||||
import DevicesDialog from '@/components/dialogs/DevicesDialog.vue'
|
||||
import { ConfigFileSection } from '@/store/files/types'
|
||||
|
||||
@Component({
|
||||
components: { DevicesDialog, Panel, CodemirrorAsync },
|
||||
@ -149,6 +187,9 @@ import DevicesDialog from '@/components/dialogs/DevicesDialog.vue'
|
||||
export default class TheEditor extends Mixins(BaseMixin) {
|
||||
dialogConfirmChange = false
|
||||
dialogDevices = false
|
||||
fileStructureSidebar = true
|
||||
structureActive: number[] = []
|
||||
structureOpen: number[] = []
|
||||
|
||||
formatFilesize = formatFilesize
|
||||
|
||||
@ -164,10 +205,10 @@ export default class TheEditor extends Mixins(BaseMixin) {
|
||||
mdiFileDocumentEditOutline = mdiFileDocumentEditOutline
|
||||
mdiFileDocumentOutline = mdiFileDocumentOutline
|
||||
mdiUsb = mdiUsb
|
||||
mdiFormatListCheckbox = mdiFormatListCheckbox
|
||||
|
||||
declare $refs: {
|
||||
editor: Codemirror
|
||||
}
|
||||
//@ts-ignore
|
||||
@Ref('editor') editor!: CodemirrorAsync
|
||||
|
||||
get changed() {
|
||||
return this.$store.state.editor.changed ?? false
|
||||
@ -305,6 +346,48 @@ export default class TheEditor extends Mixins(BaseMixin) {
|
||||
return url
|
||||
}
|
||||
|
||||
get configFileStructure() {
|
||||
if (!['conf', 'cfg'].includes(this.fileExtension)) {
|
||||
this.fileStructureSidebar = false
|
||||
return null
|
||||
}
|
||||
|
||||
const lines = this.sourcecode.split(/\n/gi)
|
||||
const regex = /^[^#\S]*?(\[(?<section>.*?)]|(?<name>\w+)\s*?[:=])/gim
|
||||
const structure: ConfigFileSection[] = []
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
const matches = [...line.matchAll(regex)]
|
||||
|
||||
// break if no matches were found
|
||||
if (matches.length === 0) continue
|
||||
|
||||
const match = matches[0]
|
||||
if (match['groups']['section']) {
|
||||
structure.push({
|
||||
name: match['groups']['section'],
|
||||
type: 'section',
|
||||
line: i + 1,
|
||||
children: [],
|
||||
})
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (match['groups']['name']) {
|
||||
structure[structure.length - 1]['children'].push({
|
||||
name: match['groups']['name'],
|
||||
type: 'item',
|
||||
line: i + 1,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.fileStructureSidebar = true
|
||||
return structure
|
||||
}
|
||||
|
||||
cancelDownload() {
|
||||
this.$store.dispatch('editor/cancelLoad')
|
||||
}
|
||||
@ -337,6 +420,29 @@ export default class TheEditor extends Mixins(BaseMixin) {
|
||||
})
|
||||
}
|
||||
|
||||
showFileStructure() {
|
||||
this.fileStructureSidebar = !this.fileStructureSidebar
|
||||
}
|
||||
|
||||
activeChanges(key: any) {
|
||||
this.editor?.gotoLine(key)
|
||||
}
|
||||
|
||||
lineChanges(line: number) {
|
||||
this.configFileStructure?.map((item) => {
|
||||
if (item.line == line) {
|
||||
this.structureActive = [line]
|
||||
} else {
|
||||
item.children?.map((child) => {
|
||||
if (child.line == line) {
|
||||
this.structureActive = [line]
|
||||
if (!this.structureOpen.includes(item.line)) this.structureOpen.push(item.line)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Watch('changed')
|
||||
changedChanged(newVal: boolean) {
|
||||
if (!this.confirmUnsavedChanges) return
|
||||
@ -398,4 +504,23 @@ export default class TheEditor extends Mixins(BaseMixin) {
|
||||
background-color: var(--color-primary);
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E %3Cpath d='M15.88 8.29L10 14.17l-1.88-1.88a.996.996 0 1 0-1.41 1.41l2.59 2.59c.39.39 1.02.39 1.41 0L17.3 9.7a.996.996 0 0 0 0-1.41c-.39-.39-1.03-.39-1.42 0z' fill='%23fffff'/%3E %3C/svg%3E");
|
||||
}
|
||||
|
||||
@media screen and (min-width: 960px) {
|
||||
.codemirror {
|
||||
width: calc(100% - 300px);
|
||||
}
|
||||
}
|
||||
.structure-sidebar {
|
||||
width: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
._structure-sidebar-item {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
::v-deep .v-treeview-node__level + .v-treeview-node__level {
|
||||
width: 12px;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div class="vue-codemirror">
|
||||
<div ref="codemirror" v-observe-visibility="visibilityChanged"></div>
|
||||
<div ref="editor" v-observe-visibility="visibilityChanged"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
// Inspired by these repo: https://github.com/surmon-china/vue-codemirror
|
||||
// Inspired by this repo: https://github.com/surmon-china/vue-codemirror
|
||||
|
||||
import { Component, Mixins, Prop, Watch } from 'vue-property-decorator'
|
||||
import { Component, Mixins, Prop, Ref, Watch } from 'vue-property-decorator'
|
||||
import BaseMixin from '../mixins/base'
|
||||
import { basicSetup } from 'codemirror'
|
||||
import { EditorView, keymap } from '@codemirror/view'
|
||||
@ -27,9 +27,7 @@ export default class Codemirror extends Mixins(BaseMixin) {
|
||||
private codemirror: null | EditorView = null
|
||||
private cminstance: null | EditorView = null
|
||||
|
||||
declare $refs: {
|
||||
codemirror: HTMLElement
|
||||
}
|
||||
@Ref('editor') editor!: HTMLElement
|
||||
|
||||
@Prop({ required: false, default: '' })
|
||||
declare readonly code: string
|
||||
@ -65,7 +63,7 @@ export default class Codemirror extends Mixins(BaseMixin) {
|
||||
|
||||
initialize() {
|
||||
this.codemirror = new EditorView({
|
||||
parent: this.$refs.codemirror,
|
||||
parent: this.editor,
|
||||
})
|
||||
this.cminstance = this.codemirror
|
||||
|
||||
@ -88,6 +86,10 @@ export default class Codemirror extends Mixins(BaseMixin) {
|
||||
indentUnit.of(' '.repeat(this.tabSize)),
|
||||
keymap.of([indentWithTab]),
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.selectionSet) {
|
||||
const line = this.cminstance?.state?.doc.lineAt(this.cminstance?.state?.selection.main.head).number
|
||||
this.$emit('lineChange', line)
|
||||
}
|
||||
this.content = update.state?.doc.toString()
|
||||
if (this.$emit) {
|
||||
this.$emit('input', this.content)
|
||||
@ -110,5 +112,15 @@ export default class Codemirror extends Mixins(BaseMixin) {
|
||||
get tabSize() {
|
||||
return this.$store.state.gui.editor.tabSize || 2
|
||||
}
|
||||
|
||||
gotoLine(line: number) {
|
||||
const l = this.cminstance?.state?.doc.line(line)
|
||||
if (!l) return
|
||||
|
||||
this.cminstance?.dispatch({
|
||||
selection: { head: l.from, anchor: l.to },
|
||||
scrollIntoView: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -203,6 +203,7 @@
|
||||
"Downloading": "Downloading",
|
||||
"FailedSave": "{filename} could not be uploaded!",
|
||||
"FileReadOnly": "read-only",
|
||||
"FileStructure": "File Structure",
|
||||
"SaveClose": "Save & close",
|
||||
"SaveRestart": "Save & Restart",
|
||||
"SuccessfullySaved": "{filename} successfully saved.",
|
||||
|
@ -197,6 +197,7 @@
|
||||
"Downloading": "正在下载",
|
||||
"FailedSave": "上传{filename}失败!",
|
||||
"FileReadOnly": "只读文件",
|
||||
"FileStructure": "结构",
|
||||
"SaveClose": "保存并关闭",
|
||||
"SaveRestart": "保存并重启",
|
||||
"SuccessfullySaved": "{filename}保存成功!",
|
||||
|
@ -97,3 +97,13 @@ export interface ApiGetDirectoryReturnFile {
|
||||
filename: string
|
||||
permissions: string
|
||||
}
|
||||
|
||||
export interface ConfigFileKey {
|
||||
name: string
|
||||
type: string
|
||||
line: number
|
||||
}
|
||||
|
||||
export interface ConfigFileSection extends ConfigFileKey {
|
||||
children: ConfigFileKey[]
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user