Skip to content

Commit

Permalink
feat: adds a file structure sidebar in the editor (#1943)
Browse files Browse the repository at this point in the history
Co-authored-by: Stefan Dej <meteyou@gmail.com>
  • Loading branch information
CF3B5 and meteyou authored Sep 7, 2024
1 parent f6f3c77 commit 695832a
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 15 deletions.
141 changes: 133 additions & 8 deletions src/components/TheEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 />
Expand Down Expand Up @@ -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'
Expand All @@ -139,16 +176,20 @@ 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 },
})
export default class TheEditor extends Mixins(BaseMixin) {
dialogConfirmChange = false
dialogDevices = false
fileStructureSidebar = true
structureActive: number[] = []
structureOpen: number[] = []
formatFilesize = formatFilesize
Expand All @@ -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
Expand Down Expand Up @@ -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')
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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>
26 changes: 19 additions & 7 deletions src/components/inputs/Codemirror.vue
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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>
1 change: 1 addition & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
1 change: 1 addition & 0 deletions src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@
"Downloading": "正在下载",
"FailedSave": "上传{filename}失败!",
"FileReadOnly": "只读文件",
"FileStructure": "结构",
"SaveClose": "保存并关闭",
"SaveRestart": "保存并重启",
"SuccessfullySaved": "{filename}保存成功!",
Expand Down
10 changes: 10 additions & 0 deletions src/store/files/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
}

0 comments on commit 695832a

Please sign in to comment.