Skip to content

Feat/editor#76

Open
jonasstrehle wants to merge 11 commits intorelease/0.0.1from
feat/editor
Open

Feat/editor#76
jonasstrehle wants to merge 11 commits intorelease/0.0.1from
feat/editor

Conversation

@jonasstrehle
Copy link
Member

No description provided.

Copilot AI review requested due to automatic review settings March 3, 2026 19:25
@jonasstrehle jonasstrehle changed the base branch from main to release/0.0.1 March 3, 2026 19:25
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new /editor route that hosts a Monaco-based editor workspace with a file-tree sidebar, tabs, drag/drop, and context-menu actions, supported by new composables and UI primitives.

Changes:

  • Introduces EditorView + wrapper view and wires it into the router at /editor.
  • Adds editor sidebar components (tree items, tabs, context menu) plus composables for selection, clipboard, platform shortcuts, and drag/drop state.
  • Adds UI wrapper components for reka-ui resizable panels and scroll areas; updates dependencies (modern-monaco, reka-ui, @vueuse/core).

Reviewed changes

Copilot reviewed 20 out of 22 changed files in this pull request and generated 18 comments.

Show a summary per file
File Description
src/views/Editor/EditorViewWrapper.vue Wrapper to mount the new editor view.
src/views/Editor/EditorView.vue Main editor page: workspace FS tree, tab persistence, drag/drop, clipboard operations.
src/types/FileTree.ts Defines file tree node shape used across editor components.
src/router/index.ts Adds /editor route entry.
src/composable/usePlatform.ts OS-aware shortcut formatting (Mac symbols vs Ctrl/Alt/Shift).
src/composable/useFileSelection.ts VS Code-style multi-selection behavior for the tree.
src/composable/useFileDragDrop.ts Shared drag state + helpers for intra-tree drag/drop.
src/composable/useFileClipboard.ts Internal cut/copy/paste state + “copy path” helpers.
src/composable/useEditorTabs.ts Manages open/active/preview tabs and persistence hooks.
src/components/ui/scroll-area/* New scroll-area wrappers around reka-ui.
src/components/ui/resizable/* New resizable panel wrappers around reka-ui.
src/components/Editor/FolderContextMenu.vue Context menu UI for file/folder/background actions.
src/components/Editor/FileTreeItem.vue Recursive tree item renderer with drag/drop + inline create/rename.
src/components/Editor/EditorTabs.vue Tab strip UI for open files.
src/components/Editor/EditorSidebar.vue Sidebar container: selection, context menu wiring, keyboard shortcuts, root creation.
package.json / package-lock.json Adds/bumps dependencies for Monaco + reka-ui/vueuse updates.
.gitignore Ignores local “instructions” files.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +287 to +288
const parts = oldPath.split('/');
parts[parts.length - 1] = newName;
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

newName is inserted into the path via oldPath.split('/') / join('/') without validation. If the user enters / (or an empty segment), this becomes a move across directories rather than a rename and can produce invalid paths. Consider rejecting names containing / (and optionally .. / leading dots) before calling fs.rename.

Suggested change
const parts = oldPath.split('/');
parts[parts.length - 1] = newName;
const trimmedName = newName.trim();
if (
!trimmedName ||
trimmedName.includes('/') ||
trimmedName === '.' ||
trimmedName === '..'
) {
console.error('Invalid new name for rename:', newName);
return;
}
const parts = oldPath.split('/');
parts[parts.length - 1] = trimmedName;

Copilot uses AI. Check for mistakes.
Comment on lines +2 to +15
import type { ScrollAreaRootProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
ScrollAreaCorner,
ScrollAreaRoot,
ScrollAreaViewport,
} from "reka-ui"
import { cn } from "@/lib/utils"
import ScrollBar from "./ScrollBar.vue"

const props = defineProps<ScrollAreaRootProps & { class?: HTMLAttributes["class"] }>()

const delegatedProps = reactiveOmit(props, "class")
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

The script block uses double quotes and omits semicolons, which conflicts with the repo’s Prettier config (.prettierrc.json enforces singleQuote and semi). Please run Prettier or update formatting here to avoid CI/lint churn.

Suggested change
import type { ScrollAreaRootProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
ScrollAreaCorner,
ScrollAreaRoot,
ScrollAreaViewport,
} from "reka-ui"
import { cn } from "@/lib/utils"
import ScrollBar from "./ScrollBar.vue"
const props = defineProps<ScrollAreaRootProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
import type { ScrollAreaRootProps } from 'reka-ui';
import type { HTMLAttributes } from 'vue';
import { reactiveOmit } from '@vueuse/core';
import {
ScrollAreaCorner,
ScrollAreaRoot,
ScrollAreaViewport,
} from 'reka-ui';
import { cn } from '@/lib/utils';
import ScrollBar from './ScrollBar.vue';
const props = defineProps<ScrollAreaRootProps & { class?: HTMLAttributes['class'] }>();
const delegatedProps = reactiveOmit(props, 'class');

Copilot uses AI. Check for mistakes.
Comment on lines +41 to +64
// ── Viewport-aware positioning ─────────────────────────────────────
const menuRef = ref<HTMLDivElement | null>(null);
const adjustedX = ref(props.x);
const adjustedY = ref(props.y);

onMounted(async () => {
await nextTick();
if (!menuRef.value) return;

const rect = menuRef.value.getBoundingClientRect();
const vw = window.innerWidth;
const vh = window.innerHeight;
const padding = 8;

// Flip left if menu overflows right edge
if (props.x + rect.width + padding > vw) {
adjustedX.value = Math.max(padding, props.x - rect.width);
}

// Flip up if menu overflows bottom edge
if (props.y + rect.height + padding > vh) {
adjustedY.value = Math.max(padding, props.y - rect.height);
}
});
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

Positioning is only calculated in onMounted() and adjustedX/adjustedY are initialized once from props.x/y. If the context menu is opened again while this component instance stays mounted (e.g., right-click a different spot while menu is already open), the menu won't reposition. Consider watching props.x/props.y (and re-running the overflow logic after nextTick) or keying the component by x,y,nodeType to force a remount.

Copilot uses AI. Check for mistakes.
Comment on lines +67 to +69
/** Whether the menu was opened on a specific file/folder node */
const hasNode = props.nodeType !== null;

Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

hasNode is derived from props.nodeType once at setup time, so it won’t update if nodeType changes while the menu stays mounted. Make this a computed(() => props.nodeType !== null) (or avoid caching) so the menu content matches the current props.

Copilot uses AI. Check for mistakes.
Comment on lines +334 to +347
async function handlePasteItems(targetPath: string, sourcePaths: string[], mode: 'cut' | 'copy') {
try {
const targetNode = findNode(tree.value, targetPath);
const targetDir = targetNode?.type === 'folder'
? targetPath
: targetPath.substring(0, targetPath.lastIndexOf('/')) || '/';

for (const srcPath of sourcePaths) {
const destPath = await resolveDestPath(srcPath, targetDir, mode);
if (!destPath) continue;

if (mode === 'cut') {
await workspace.fs.rename(srcPath, destPath);

Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

handlePasteItems doesn’t guard against illegal moves like cutting a folder into itself / one of its descendants (or into the same parent). The drag/drop path uses isInvalidDrop, but paste currently doesn’t, so it can attempt invalid fs.rename operations. Reuse isInvalidDrop(srcPath, targetDir) for mode === 'cut' before calling rename.

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +65
@click="emit('tab-click', path)"
@dblclick="emit('tab-click', path)"
@mousedown="handleMiddleClick($event, path)"
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

The @dblclick handler currently emits tab-click again, so double-clicking can't trigger a separate “pin” action. Emit a dedicated tab-dblclick event here (and keep single-click behavior unchanged).

Copilot uses AI. Check for mistakes.
Comment on lines +291 to +298
await workspace.fs.rename(oldPath, newPath);
await reloadTree();

tabs.renameOpenFile(oldPath, newPath);
saveTabs();

if (tabs.activeFile.value === newPath) {
await workspace.openTextDocument(newPath);
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

tabs.renameOpenFile(oldPath, newPath) only updates an exact tab path. If oldPath is a folder, any open files under that folder will keep stale paths after fs.rename, and the active tab could point to a non-existent document. Consider detecting folder renames and updating all openFilesList entries that start with oldPath + '/' (and the active file) to the corresponding new prefix.

Suggested change
await workspace.fs.rename(oldPath, newPath);
await reloadTree();
tabs.renameOpenFile(oldPath, newPath);
saveTabs();
if (tabs.activeFile.value === newPath) {
await workspace.openTextDocument(newPath);
const previousActive = tabs.activeFile.value;
await workspace.fs.rename(oldPath, newPath);
await reloadTree();
// Update an exact open file match (file rename case)
tabs.renameOpenFile(oldPath, newPath);
// Additionally handle folder renames: update any open files under the folder
const oldPrefix = oldPath + '/';
const newPrefix = newPath + '/';
const currentOpenFiles = tabs.openFilesList.value;
const updatedOpenFiles = currentOpenFiles.map((openPath) => {
if (openPath.startsWith(oldPrefix)) {
return newPrefix + openPath.slice(oldPrefix.length);
}
return openPath;
});
// Only assign back if something actually changed
let openFilesChanged = false;
if (updatedOpenFiles.length === currentOpenFiles.length) {
for (let i = 0; i < updatedOpenFiles.length; i++) {
if (updatedOpenFiles[i] !== currentOpenFiles[i]) {
openFilesChanged = true;
break;
}
}
}
if (openFilesChanged) {
tabs.openFilesList.value = updatedOpenFiles;
}
// Update the active file if it was under the renamed folder
if (tabs.activeFile.value && tabs.activeFile.value.startsWith(oldPrefix)) {
tabs.activeFile.value =
newPrefix + tabs.activeFile.value.slice(oldPrefix.length);
}
saveTabs();
// Reopen the active document if its path changed due to the rename
if (tabs.activeFile.value && tabs.activeFile.value !== previousActive) {
await workspace.openTextDocument(tabs.activeFile.value);

Copilot uses AI. Check for mistakes.
Comment on lines +348 to +351
tabs.renameOpenFile(srcPath, destPath);
if (tabs.activeFile.value === destPath) {
await workspace.openTextDocument(destPath);
}
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

In the cut/move path, tabs.renameOpenFile(srcPath, destPath) only updates a single exact path. If srcPath is a folder, open tabs for files inside that folder won’t be updated after the move, leaving broken tab paths. Consider renaming all open paths with the srcPath + '/' prefix (and updating activeFile/previewFile if needed).

Suggested change
tabs.renameOpenFile(srcPath, destPath);
if (tabs.activeFile.value === destPath) {
await workspace.openTextDocument(destPath);
}
const previousActive = tabs.activeFile.value;
const openFiles = [...tabs.openFilesList.value];
for (const openPath of openFiles) {
if (openPath === srcPath) {
// Exact match: move to destPath
tabs.renameOpenFile(openPath, destPath);
} else if (openPath.startsWith(srcPath + '/')) {
// Path inside moved folder: preserve relative suffix
const suffix = openPath.slice(srcPath.length); // includes leading '/'
const newPath = destPath + suffix;
tabs.renameOpenFile(openPath, newPath);
}
}
if (tabs.activeFile.value && tabs.activeFile.value !== previousActive) {
await workspace.openTextDocument(tabs.activeFile.value);
}

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +3
export { default as ResizableHandle } from "./ResizableHandle.vue"
export { default as ResizablePanelGroup } from "./ResizablePanelGroup.vue"
export { SplitterPanel as ResizablePanel } from "reka-ui"
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

This index file uses double quotes and omits semicolons, which conflicts with the repo’s Prettier config (.prettierrc.json has singleQuote: true, semi: true). Please format to match the existing components/ui/*/index.ts exports (e.g. src/components/ui/accordion/index.ts).

Suggested change
export { default as ResizableHandle } from "./ResizableHandle.vue"
export { default as ResizablePanelGroup } from "./ResizablePanelGroup.vue"
export { SplitterPanel as ResizablePanel } from "reka-ui"
export { default as ResizableHandle } from './ResizableHandle.vue';
export { default as ResizablePanelGroup } from './ResizablePanelGroup.vue';
export { SplitterPanel as ResizablePanel } from 'reka-ui';

Copilot uses AI. Check for mistakes.
Comment on lines +181 to +203
function handleContextNewFile() {
closeContextMenu();
if (contextMenu.value?.node) {
// Create inside a specific folder
const folderPath = contextMenu.value.node.path;
emit('ensure-expand', folderPath);
nextTick(() => { creatingIn.value = { folderPath, type: 'file' }; });
} else {
// Background right-click → create at root
startCreateFileAtRoot();
}
}

function handleContextNewFolder() {
closeContextMenu();
if (contextMenu.value?.node) {
const folderPath = contextMenu.value.node.path;
emit('ensure-expand', folderPath);
nextTick(() => { creatingIn.value = { folderPath, type: 'folder' }; });
} else {
startCreateFolderAtRoot();
}
}
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

handleContextNewFile / handleContextNewFolder call closeContextMenu() before checking contextMenu.value?.node, so contextMenu.value is always null and the folder-specific creation path will never run. Capture contextMenu.value?.node (or the folder path) into a local variable before closing, or close the menu after branching.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants