Skip to content

feat: add file renaming #63

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 50 additions & 32 deletions src/editor/FileSelector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { computed, inject, ref, VNode, Ref } from 'vue'

const store = inject('store') as Store

const pending = ref(false)
const pending = ref<boolean | string>(false)
const pendingFilename = ref('Comp.vue')
const importMapFile = 'import-map.json'
const showImportMap = inject('import-map') as Ref<boolean>
Expand Down Expand Up @@ -36,17 +36,18 @@ function startAddFile() {
pending.value = true
}

function cancelAddFile() {
function cancelNameFile() {
pending.value = false
}

function focus({ el }: VNode) {
;(el as HTMLInputElement).focus()
}

function doneAddFile() {
function doneNameFile() {
if (!pending.value) return
const filename = pendingFilename.value
const oldFilename = pending.value === true ? '' : pending.value

if (!/\.(vue|js|ts|css|json)$/.test(filename)) {
store.state.errors = [
Expand All @@ -55,14 +56,28 @@ function doneAddFile() {
return
}

if (filename in store.state.files) {
if (filename !== oldFilename && filename in store.state.files) {
store.state.errors = [`File "${filename}" already exists.`]
return
}

store.state.errors = []
cancelAddFile()
store.addFile(filename)
cancelNameFile()

if (filename === oldFilename) {
return
}

if (oldFilename) {
store.renameFile(oldFilename, filename)
} else {
store.addFile(filename)
}
}

function editFileName(file: string) {
pendingFilename.value = file
pending.value = file
}

const fileSel = ref(null)
Expand All @@ -85,32 +100,35 @@ function horizontalScroll(e: WheelEvent) {
@wheel="horizontalScroll"
ref="fileSel"
>
<div
v-for="(file, i) in files"
class="file"
:class="{ active: store.state.activeFile.filename === file }"
@click="store.setActive(file)"
>
<span class="label">{{
file === importMapFile ? 'Import Map' : file
}}</span>
<span v-if="i > 0" class="remove" @click.stop="store.deleteFile(file)">
<svg class="icon" width="12" height="12" viewBox="0 0 24 24">
<line stroke="#999" x1="18" y1="6" x2="6" y2="18"></line>
<line stroke="#999" x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</span>
</div>
<div v-if="pending" class="file pending">
<input
v-model="pendingFilename"
spellcheck="false"
@blur="doneAddFile"
@keyup.enter="doneAddFile"
@keyup.esc="cancelAddFile"
@vue:mounted="focus"
/>
</div>
<template v-for="(file, i) in files">
<div
v-if="pending !== file"
class="file"
:class="{ active: store.state.activeFile.filename === file }"
@click="store.setActive(file)"
@dblclick="i > 0 && editFileName(file)"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let us check it by adding a new property of File, like renamable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see why that might be desirable, but what I've done here is consistent with deleting a file.

My reasoning is that it's already possible to rename a file by deleting it and re-adding it. It's painful for the user, but it is possible. The only restriction we currently place on that in the UI is preventing the first file from being deleted. So I've maintained the same restriction for renaming.

I think there is potential for providing various forms of permissions, to restrict what files can be added/removed/renamed, but currently we don't have anything like that and I can't think of a reason why it would be necessary to add it specifically for renaming when it isn't available for deleting.

>
<span class="label">{{
file === importMapFile ? 'Import Map' : file
}}</span>
<span v-if="i > 0" class="remove" @click.stop="store.deleteFile(file)">
<svg class="icon" width="12" height="12" viewBox="0 0 24 24">
<line stroke="#999" x1="18" y1="6" x2="6" y2="18"></line>
<line stroke="#999" x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</span>
</div>
<div v-if="(pending === true && i === files.length - 1) || (pending === file)" class="file pending">
<input
v-model="pendingFilename"
spellcheck="false"
@blur="doneNameFile"
@keyup.enter="doneNameFile"
@keyup.esc="cancelNameFile"
@vue:mounted="focus"
/>
</div>
</template>
<button class="add" @click="startAddFile">+</button>

<div v-if="showImportMap" class="import-map-wrapper">
Expand Down
37 changes: 37 additions & 0 deletions src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export interface Store {
setActive: (filename: string) => void
addFile: (filename: string | File) => void
deleteFile: (filename: string) => void
renameFile: (oldFilename: string, newFilename: string) => void
getImportMap: () => any
initialShowOutput: boolean
initialOutputMode: OutputModes
Expand Down Expand Up @@ -167,6 +168,42 @@ export class ReplStore implements Store {
}
}

renameFile(oldFilename: string, newFilename: string) {
const { files } = this.state
const file = files[oldFilename]

if (!file) {
this.state.errors = [`Could not rename "${oldFilename}", file not found`]
return
}

if (!newFilename || oldFilename === newFilename) {
this.state.errors = [`Cannot rename "${oldFilename}" to "${newFilename}"`]
return
}

file.filename = newFilename

const newFiles: Record<string, File> = {}

// Preserve iteration order for files
for (const name in files) {
if (name === oldFilename) {
newFiles[newFilename] = file
} else {
newFiles[name] = files[name]
}
}

this.state.files = newFiles

if (this.state.mainFile === oldFilename) {
this.state.mainFile = newFilename
}

compileFile(this, file)
}

serialize() {
const files = this.getFiles()
const importMap = files['import-map.json']
Expand Down