diff --git a/packages/runtime/src/store/editor.spec.ts b/packages/runtime/src/store/editor.spec.ts new file mode 100644 index 00000000..ec0eac20 --- /dev/null +++ b/packages/runtime/src/store/editor.spec.ts @@ -0,0 +1,102 @@ +import { expect, test } from 'vitest'; + +import { EditorStore } from './editor.js'; +import type { File } from '@tutorialkit/types'; + +test('initial files are sorted', () => { + const store = new EditorStore(); + store.setDocuments({ + 'test/math.test.ts': '', + '.gitignore': '', + 'src/math.ts': '', + 'src/geometry.ts': '', + }); + + expect(store.files.get().map(toFilename)).toMatchInlineSnapshot(` + [ + "src/geometry.ts", + "src/math.ts", + "test/math.test.ts", + ".gitignore", + ] + `); +}); + +test('added files are sorted', () => { + const store = new EditorStore(); + store.setDocuments({ + 'test/math.test.ts': '', + '.gitignore': '', + 'src/math.ts': '', + }); + + store.addFileOrFolder({ path: 'something.ts', type: 'FILE' }); + store.addFileOrFolder({ path: 'another.css', type: 'FILE' }); + store.addFileOrFolder({ path: 'no-extension', type: 'FILE' }); + + expect(store.files.get().map(toFilename)).toMatchInlineSnapshot(` + [ + "src/math.ts", + "test/math.test.ts", + ".gitignore", + "another.css", + "no-extension", + "something.ts", + ] + `); +}); + +test('added folders are sorted', () => { + const store = new EditorStore(); + store.setDocuments({ + 'test/math.test.ts': '', + '.gitignore': '', + 'src/math.ts': '', + }); + + store.addFileOrFolder({ path: 'src/components', type: 'FOLDER' }); + store.addFileOrFolder({ path: 'src/utils', type: 'FOLDER' }); + store.addFileOrFolder({ path: 'test/unit', type: 'FOLDER' }); + store.addFileOrFolder({ path: 'e2e', type: 'FOLDER' }); + + expect(store.files.get().map(toFilename)).toMatchInlineSnapshot(` + [ + "e2e", + "src/components", + "src/utils", + "src/math.ts", + "test/unit", + "test/math.test.ts", + ".gitignore", + ] + `); +}); + +test('empty directories are removed when new content is added', () => { + const store = new EditorStore(); + store.setDocuments({ + 'src/index.ts': '', + }); + + store.addFileOrFolder({ path: 'src/components', type: 'FOLDER' }); + + expect(store.files.get().map(toFilename)).toMatchInlineSnapshot(` + [ + "src/components", + "src/index.ts", + ] + `); + + store.addFileOrFolder({ path: 'src/components/FileTree', type: 'FOLDER' }); + + expect(store.files.get().map(toFilename)).toMatchInlineSnapshot(` + [ + "src/components/FileTree", + "src/index.ts", + ] + `); +}); + +function toFilename(file: File) { + return file.path; +} diff --git a/packages/runtime/src/store/editor.ts b/packages/runtime/src/store/editor.ts index 1bd1031f..5800613b 100644 --- a/packages/runtime/src/store/editor.ts +++ b/packages/runtime/src/store/editor.ts @@ -25,7 +25,7 @@ export class EditorStore { files = computed(this.documents, (documents) => Object.entries(documents) .map(([path, doc]) => ({ path, type: doc?.type || 'FILE' })) - .sort(), + .sort(sortFiles), ); currentDocument = computed([this.documents, this.selectedFile], (documents, selectedFile) => { if (!selectedFile) { @@ -101,7 +101,7 @@ export class EditorStore { addFileOrFolder(file: File) { // when adding file or folder to empty folder, remove the empty folder from documents - const emptyFolder = this.files.value?.find((f) => file.path.startsWith(f.path)); + const emptyFolder = this.files.get().find((f) => f.type === 'FOLDER' && file.path.startsWith(f.path)); if (emptyFolder && emptyFolder.type === 'FOLDER') { this.documents.setKey(emptyFolder.path, undefined); @@ -169,3 +169,46 @@ export class EditorStore { }; } } + +function sortFiles(fileA: File, fileB: File) { + const segmentsA = fileA.path.split('/'); + const segmentsB = fileB.path.split('/'); + const minLength = Math.min(segmentsA.length, segmentsB.length); + + for (let i = 0; i < minLength; i++) { + const a = toFileSegment(fileA, segmentsA, i); + const b = toFileSegment(fileB, segmentsB, i); + + // folders are always shown before files + if (a.type !== b.type) { + return a.type === 'FOLDER' ? -1 : 1; + } + + const comparison = compareString(a.path, b.path); + + // either folder name changed or last segments are compared + if (comparison !== 0 || a.isLast || b.isLast) { + return comparison; + } + } + + throw new Error(JSON.stringify({ fileA, fileB })); +} + +function toFileSegment(file: File, segments: string[], current: number) { + const isLast = segments[current + 1] === undefined; + + return { path: segments[current], type: isLast ? file.type : 'FOLDER', isLast }; +} + +function compareString(a: string, b: string) { + if (a < b) { + return -1; + } + + if (a > b) { + return 1; + } + + return 0; +}