From fcc761757b9626869b1dd7f855ba0970988ad68f Mon Sep 17 00:00:00 2001 From: Tiago Padilha Date: Tue, 27 May 2025 10:06:09 -0300 Subject: [PATCH 01/21] feat: implement optimized SSH service for improved file navigation and operations --- src/store/fileExplorer.ts | 228 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 src/store/fileExplorer.ts diff --git a/src/store/fileExplorer.ts b/src/store/fileExplorer.ts new file mode 100644 index 0000000..9963175 --- /dev/null +++ b/src/store/fileExplorer.ts @@ -0,0 +1,228 @@ +import { defineStore } from 'pinia'; +import { ref, reactive } from 'vue'; +import { optimizedSshService } from '@/services/optimized-ssh-service'; +import type { SshConnection } from '@/types/ssh-connection'; + +export interface FileEntry { + name: string; + type: 'file' | 'directory'; + size: number; + modTime: number; + permissions: string; + owner: string; + group: string; +} + +export interface DirectoryTreeNode { + name: string; + path: string; + type: 'directory' | 'file'; + expanded?: boolean; + children?: DirectoryTreeNode[]; +} + +export interface DirectoryNode { + name: string; + path: string; + isExpanded: boolean; + isLoading: boolean; + children: DirectoryNode[]; + files: FileEntry[]; + isLoaded: boolean; +} + +interface FileExplorerState { + currentPath: string; + projectRoot: string; + directoryTree: DirectoryNode[]; + expandedPaths: Set; + lastConnection: SshConnection | null; +} + +export const useFileExplorerStore = defineStore('fileExplorer', () => { + const state = reactive({ + currentPath: '', + projectRoot: '', + directoryTree: [], + expandedPaths: new Set(), + lastConnection: null + }); + + const isLoading = ref(false); + const error = ref(''); + + const getCurrentDirectoryFiles = () => { + const node = findNodeByPath(state.currentPath); + return node?.files || []; + }; + + const getDirectoryTree = (): DirectoryTreeNode[] => { + return state.directoryTree.map(convertToTreeNode); + }; + + function convertToTreeNode(node: DirectoryNode): DirectoryTreeNode { + let displayName = node.name; + if (node.path === state.projectRoot) { + displayName = 'root'; + } + + const allChildren: DirectoryTreeNode[] = [ + ...node.children.map(convertToTreeNode), + ...node.files + .filter((file) => file.type === 'file') + .map((file) => { + const filePath = node.path.endsWith('/') + ? `${node.path}${file.name}` + : `${node.path}/${file.name}`; + return { + name: file.name, + path: filePath, + type: 'file' as const, + expanded: undefined, + children: [] + }; + }) + ]; + + return { + name: displayName, + path: node.path, + type: 'directory', + expanded: node.isExpanded, + children: allChildren + }; + } + async function initializeForConnection( + connection: SshConnection, + rootPath: string + ) { + state.lastConnection = connection; + state.projectRoot = rootPath; + + if (!state.currentPath || !state.currentPath.startsWith(rootPath)) { + state.currentPath = rootPath; + } + + if (state.directoryTree.length === 0) { + await initializeRoot(); + } + } + + function setCurrentPath(path: string) { + state.currentPath = path; + } + + function addToExpandedPaths(path: string) { + state.expandedPaths.add(path); + } + + function removeFromExpandedPaths(path: string) { + state.expandedPaths.delete(path); + } + + function findNodeByPath(path: string): DirectoryNode | null { + function searchTree(nodes: DirectoryNode[]): DirectoryNode | null { + for (const node of nodes) { + if (node.path === path) { + return node; + } + const found = searchTree(node.children); + if (found) return found; + } + return null; + } + return searchTree(state.directoryTree); + } + + async function expandDirectory(path: string): Promise { + if (!state.lastConnection) return; + + const node = findNodeByPath(path); + if (!node) return; + + if (node.isLoaded && node.isExpanded) { + node.isExpanded = false; + removeFromExpandedPaths(path); + return; + } + + node.isLoading = true; + + try { + const files = await optimizedSshService.list( + state.lastConnection, + path + ); + + const directories = files.filter((f) => f.type === 'directory'); + const fileList = files.filter((f) => f.type === 'file'); + + node.children = directories.map((dir) => { + const childPath = path.endsWith('/') + ? `${path}${dir.name}` + : `${path}/${dir.name}`; + return { + name: dir.name, + path: childPath, + isExpanded: false, + isLoading: false, + children: [], + files: [], + isLoaded: false + }; + }); + + node.files = fileList; + node.isLoaded = true; + node.isExpanded = true; + addToExpandedPaths(path); + } catch (err) { + error.value = + (err as Error).message || 'Failed to expand directory'; + } finally { + node.isLoading = false; + } + } + + async function initializeRoot(): Promise { + if (!state.lastConnection || !state.projectRoot) return; + + if (state.directoryTree.length === 0) { + const rootNode: DirectoryNode = { + name: 'root', + path: state.projectRoot, + isExpanded: false, + isLoading: false, + children: [], + files: [], + isLoaded: false + }; + + state.directoryTree = [rootNode]; + } + + await expandDirectory(state.projectRoot); + } + + function reset() { + state.currentPath = ''; + state.projectRoot = ''; + state.directoryTree = []; + state.expandedPaths.clear(); + state.lastConnection = null; + isLoading.value = false; + error.value = ''; + } + + return { + state, + isLoading, + error, + getCurrentDirectoryFiles, + getDirectoryTree, + initializeForConnection, + setCurrentPath, + expandDirectory, + reset + }; +}); From 81ddb570cbb834632fde39437cf7fba6c5c854b5 Mon Sep 17 00:00:00 2001 From: Tiago Padilha Date: Tue, 27 May 2025 10:06:23 -0300 Subject: [PATCH 02/21] feat: implement optimized SSH service with caching and improved command execution --- src/types/file-explorer.d.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/types/file-explorer.d.ts diff --git a/src/types/file-explorer.d.ts b/src/types/file-explorer.d.ts new file mode 100644 index 0000000..83c3f76 --- /dev/null +++ b/src/types/file-explorer.d.ts @@ -0,0 +1,17 @@ +export interface FileEntry { + name: string; + type: 'file' | 'directory'; + size: number; + modTime: number; + permissions: string; + owner: string; + group: string; +} + +export interface DirectoryTreeNode { + name: string; + path: string; + type: 'directory'; + expanded?: boolean; + children?: DirectoryTreeNode[]; +} From a119bd576b8f69b19e5d9235d8bf59af6a0be7e1 Mon Sep 17 00:00:00 2001 From: Tiago Padilha Date: Tue, 27 May 2025 10:06:44 -0300 Subject: [PATCH 03/21] feat: update Vite configuration to include monaco-editor and define NODE_ENV --- vite.config.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index b4352aa..db49530 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -102,7 +102,13 @@ export default defineConfig(({ command }) => { } }, optimizeDeps: { - exclude: nativeNodeModules + exclude: nativeNodeModules, + include: ['monaco-editor'] + }, + define: { + 'process.env.NODE_ENV': JSON.stringify( + process.env.NODE_ENV || 'development' + ) } }; }); From b6b6732abe275b235d233ab911838f91b73f09d7 Mon Sep 17 00:00:00 2001 From: Tiago Padilha Date: Tue, 27 May 2025 10:07:18 -0300 Subject: [PATCH 04/21] feat: configure Monaco editor on SQL editor initialization --- src/composables/useSQLEditor.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/composables/useSQLEditor.ts b/src/composables/useSQLEditor.ts index 7d4d652..d87ac08 100644 --- a/src/composables/useSQLEditor.ts +++ b/src/composables/useSQLEditor.ts @@ -1,6 +1,9 @@ import * as monaco from 'monaco-editor'; +import { configureMonaco } from '@/utils/monaco-config'; import { format } from 'sql-formatter'; import { ref, onMounted, onBeforeUnmount, watch } from 'vue'; + +configureMonaco(); import { useConnectionsStore } from '@/store/connections'; import { useSqlResultsStore } from '@/store/sqlResults'; import { AIService } from '@/services/aiService'; From bb947c2a64066fbbf4208dc2e48921a7e7a7ea5c Mon Sep 17 00:00:00 2001 From: Tiago Padilha Date: Tue, 27 May 2025 10:08:17 -0300 Subject: [PATCH 05/21] feat: enhance SSH service with optimized command execution and file operations --- electron/modules/ssh.ts | 170 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 168 insertions(+), 2 deletions(-) diff --git a/electron/modules/ssh.ts b/electron/modules/ssh.ts index 57ba20f..871a58b 100644 --- a/electron/modules/ssh.ts +++ b/electron/modules/ssh.ts @@ -3,13 +3,13 @@ import * as path from 'path'; import { testConnection, closeConnection, - executeCommand, readRemoteFile, writeRemoteFile, listRemoteFiles, createTunnel, closeTunnel } from '../helpers/ssh'; +import { optimizedSshManager } from '../helpers/optimized-ssh'; import { SshConnection } from '../../src/types/ssh-connection'; function registerSshHandlers() { @@ -20,7 +20,21 @@ function registerSshHandlers() { ipcMain.handle( 'ssh:execute-command', async (_, config: SshConnection, command: string) => { - return await executeCommand(config, command); + try { + return await optimizedSshManager.executeCommand( + config, + command + ); + } catch (error) { + return { + stdout: '', + stderr: + error instanceof Error + ? error.message + : 'Unknown error', + code: 1 + }; + } } ); @@ -157,6 +171,158 @@ function registerSshHandlers() { const success = closeTunnel(tunnelId); return { success }; }); + + ipcMain.handle( + 'ssh:optimized-list', + async (_, config: SshConnection, dirPath: string) => { + try { + const command = `cd '${dirPath}' && ls -1F`; + + const result = await optimizedSshManager.executeCommand( + config, + command + ); + + if (result.code !== 0) { + throw new Error( + result.stderr || 'Failed to list directory' + ); + } + + const files = parseLsOutput(result.stdout); + + return { success: true, files }; + } catch (error) { + return { + success: false, + error: + error instanceof Error ? error.message : 'Unknown error' + }; + } + } + ); + + ipcMain.handle( + 'ssh:optimized-read', + async (_, config: SshConnection, filePath: string, length?: number) => { + try { + const maxSize = 1024 * 1024; // 1MB limit + const readSize = length ? Math.min(length, maxSize) : maxSize; + const command = `head -c ${readSize} "${filePath}"`; + + const result = await optimizedSshManager.executeCommand( + config, + command + ); + + if (result.code !== 0) { + throw new Error(result.stderr || 'Failed to read file'); + } + + return { success: true, content: result.stdout }; + } catch (error) { + return { + success: false, + error: + error instanceof Error ? error.message : 'Unknown error' + }; + } + } + ); + + ipcMain.handle( + 'ssh:optimized-write', + async (_, config: SshConnection, filePath: string, content: string) => { + try { + const command = `cat > "${filePath}" << 'EOF'\n${content}\nEOF`; + + const result = await optimizedSshManager.executeCommand( + config, + command + ); + + if (result.code !== 0) { + throw new Error(result.stderr || 'Failed to write file'); + } + + return { success: true }; + } catch (error) { + return { + success: false, + error: + error instanceof Error ? error.message : 'Unknown error' + }; + } + } + ); + + ipcMain.handle( + 'ssh:optimized-exists', + async (_, config: SshConnection, path: string) => { + try { + const command = `test -e "${path}" && echo "exists" || echo "not_exists"`; + const result = await optimizedSshManager.executeCommand( + config, + command + ); + + return { + success: true, + exists: result.stdout.trim() === 'exists' + }; + } catch (error) { + return { + success: false, + exists: false, + error: + error instanceof Error ? error.message : 'Unknown error' + }; + } + } + ); + + ipcMain.handle('ssh:connection-stats', () => { + return optimizedSshManager.getConnectionStats(); + }); +} + +function parseLsOutput(output: string): any[] { + const lines = output.split('\n').filter((line) => line.trim()); + const files: any[] = []; + + for (const line of lines) { + const trimmedLine = line.trim(); + if (!trimmedLine || trimmedLine === '.' || trimmedLine === '..') + continue; + + let name = trimmedLine; + let type = 'file'; + + if (name.endsWith('/')) { + name = name.slice(0, -1); + type = 'directory'; + } else if (name.endsWith('*')) { + name = name.slice(0, -1); + type = 'file'; + } else if (name.endsWith('@')) { + name = name.slice(0, -1); + type = 'file'; + } + + if (!name || name === '.' || name === '..') continue; + + files.push({ + name, + type, + size: 0, + modTime: Date.now(), + permissions: type === 'directory' ? 'drwxr-xr-x' : '-rw-r--r--', + owner: 'user', + group: 'group' + }); + } + + return files; } export { registerSshHandlers }; From 311a6fb5eaa50d54a28f0a00da112d54a48a2086 Mon Sep 17 00:00:00 2001 From: Tiago Padilha Date: Tue, 27 May 2025 10:09:46 -0300 Subject: [PATCH 06/21] refactor: clean up SSH service code by removing unused interfaces and comments --- electron/helpers/ssh.ts | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/electron/helpers/ssh.ts b/electron/helpers/ssh.ts index 868c714..dfb01a6 100644 --- a/electron/helpers/ssh.ts +++ b/electron/helpers/ssh.ts @@ -7,21 +7,6 @@ import { SshConnection } from '../../src/types/ssh-connection'; interface SshError extends Error { code?: string; } - -interface SftpFile { - filename: string; - longname: string; - attrs: { - size: number; - mtime: number; - atime: number; - uid: number; - gid: number; - mode: number; - [key: string]: number | string | boolean | undefined; - }; -} - interface FileExplorerItem { name: string; type: 'file' | 'directory'; @@ -59,7 +44,7 @@ async function createConnection(config: SshConnection): Promise { host: config.host, port: config.port, username: config.user, // ssh2 uses username, not user - debug: (message: string) => console.log(`SSH Debug: ${message}`), + //debug: (message: string) => console.log(`SSH Debug: ${message}`), readyTimeout: 10000 }; @@ -226,9 +211,7 @@ async function listRemoteFiles( ): Promise { const client = await createConnection(config); - // Safety check to make sure we don't go outside the project directory if (!dirPath.startsWith(config.remotePath)) { - // If dirPath is outside remotePath, default to remotePath dirPath = config.remotePath; } @@ -499,9 +482,6 @@ async function createTunnel( remotePort: number, localPort: number = 0 ): Promise<{ tunnelId: string; localPort: number }> { - // This is the critical fix - always create a fresh connection for tunneling - // This ensures we don't reuse a connection that might be in a bad state - // from other operations like SFTP const sshClient = await createConnection(config); return new Promise((resolve, reject) => { From a406df9f27b075971f0f9c49152ccf96458dbd5d Mon Sep 17 00:00:00 2001 From: Tiago Padilha Date: Tue, 27 May 2025 10:10:26 -0300 Subject: [PATCH 07/21] feat: update permissions in settings to allow npm build and lint commands --- .claude/settings.local.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index dd8ca7a..012dc8b 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,7 +1,9 @@ { "permissions": { "allow": [ - "Bash(grep:*)" + "Bash(grep:*)", + "Bash(npm run build:*)", + "Bash(npm run lint)" ], "deny": [] } From 873198f3e6e34187733c8adcaa5602e996e54116 Mon Sep 17 00:00:00 2001 From: Tiago Padilha Date: Tue, 27 May 2025 10:10:33 -0300 Subject: [PATCH 08/21] feat: add script to run Larabase application in terminal --- run-in-terminal.sh | 1 + 1 file changed, 1 insertion(+) create mode 100644 run-in-terminal.sh diff --git a/run-in-terminal.sh b/run-in-terminal.sh new file mode 100644 index 0000000..0116c6f --- /dev/null +++ b/run-in-terminal.sh @@ -0,0 +1 @@ +/Applications/Larabase.app/Contents/MacOS/Larabase \ No newline at end of file From 24a6ff0d0ed989d67f327586ed7cc884703f50e6 Mon Sep 17 00:00:00 2001 From: Tiago Padilha Date: Tue, 27 May 2025 10:11:09 -0300 Subject: [PATCH 09/21] feat: configure Monaco editor on DotEnvEditor component initialization --- src/components/DotEnvEditor.vue | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/DotEnvEditor.vue b/src/components/DotEnvEditor.vue index 10aebe5..386b020 100644 --- a/src/components/DotEnvEditor.vue +++ b/src/components/DotEnvEditor.vue @@ -1,7 +1,10 @@ @@ -239,15 +261,73 @@ onMounted(() => { @close="$emit('close')" class="file-explorer-modal" > -
+
-
+ +
+ +
+
+
+
+ + + +
+
+ - -
-
-
- -
+
+ +
-
-
- - - - - - - - - - - - +
+
- Name - - Size - - Type - - Modified - - Actions -
+ - - - - - - - - + +
- + - - - .. - Directory
+
-
-
- {{ error }} -
+
+ {{ error }} +
-
-
-
- +
+
+ + + +

+ {{ selectedFile.name }} +

+
+
- -
-
-
{{ selectedFile.content }}
+
{{ selectedFile.content }}
+
From 49fb1eca79369a0658b20bfe0bf1db6474d69a61 Mon Sep 17 00:00:00 2001 From: Tiago Padilha Date: Tue, 27 May 2025 10:17:44 -0300 Subject: [PATCH 17/21] feat: integrate optimized SSH service for file operations in RemoteFileEditor and RemoteFileExplorer --- src/components/DirectoryTreeNode.vue | 152 ++++++++++++++++++++++++ src/components/DirectoryTreeSidebar.vue | 87 ++++++++++++++ 2 files changed, 239 insertions(+) create mode 100644 src/components/DirectoryTreeNode.vue create mode 100644 src/components/DirectoryTreeSidebar.vue diff --git a/src/components/DirectoryTreeNode.vue b/src/components/DirectoryTreeNode.vue new file mode 100644 index 0000000..8c5adc9 --- /dev/null +++ b/src/components/DirectoryTreeNode.vue @@ -0,0 +1,152 @@ + + + + + diff --git a/src/components/DirectoryTreeSidebar.vue b/src/components/DirectoryTreeSidebar.vue new file mode 100644 index 0000000..7813ddb --- /dev/null +++ b/src/components/DirectoryTreeSidebar.vue @@ -0,0 +1,87 @@ + + + + + From 706f48670f279a7a0dcfd29ffdf479d82a0a2acc Mon Sep 17 00:00:00 2001 From: Tiago Padilha Date: Tue, 27 May 2025 10:18:16 -0300 Subject: [PATCH 18/21] feat: add Monaco editor configuration with custom worker setup --- src/utils/monaco-config.ts | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/utils/monaco-config.ts diff --git a/src/utils/monaco-config.ts b/src/utils/monaco-config.ts new file mode 100644 index 0000000..4d4232c --- /dev/null +++ b/src/utils/monaco-config.ts @@ -0,0 +1,35 @@ +import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; +import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'; +import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'; +import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'; +import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'; + +let isConfigured = false; + +export function configureMonaco() { + if (isConfigured) return; + + (self as any).MonacoEnvironment = { + getWorker(_: any, label: string) { + if (label === 'json') { + return new jsonWorker(); + } + if (label === 'css' || label === 'scss' || label === 'less') { + return new cssWorker(); + } + if ( + label === 'html' || + label === 'handlebars' || + label === 'razor' + ) { + return new htmlWorker(); + } + if (label === 'typescript' || label === 'javascript') { + return new tsWorker(); + } + return new editorWorker(); + } + }; + + isConfigured = true; +} From 4087306735d2fe8c3377189cd462cd9ecfb773a0 Mon Sep 17 00:00:00 2001 From: Tiago Padilha Date: Tue, 27 May 2025 10:20:10 -0300 Subject: [PATCH 19/21] feat: enhance remote file explorer with directory tree navigation and state persistence --- CLAUDE.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index d229e6c..0f60c3a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -114,6 +114,54 @@ The application follows a communication pattern using Electron's IPC: - Always use toRaw for reactive objects to avoid errors like "An object could not be cloned." - Use separate loading states for initial loading vs. background refreshes to avoid UI flickering +## Remote File Explorer with Directory Tree + +The application includes an enhanced remote file explorer with sidebar navigation: + +### Features +- **Directory Tree Sidebar**: Collapsible tree structure for fast navigation through nested directories +- **State Persistence**: Remembers current directory when switching between file explorer and database views +- **File Integration**: Click files in tree to open directly in Monaco Editor +- **Connection-Aware State**: Different states maintained per SSH connection + +### Implementation Details +- **Store**: `src/store/fileExplorer.ts` - Pinia store managing directory tree state and file operations +- **Components**: + - `DirectoryTreeSidebar.vue` - Main sidebar with expand/collapse functionality + - `DirectoryTreeNode.vue` - Individual tree nodes with file/directory icons + - `RemoteFileExplorer.vue` - Main file explorer with integrated sidebar +- **State Management**: Uses `expandedPaths` Set to track opened directories and `currentPath` for navigation +- **Performance**: Leverages existing optimized SSH connection pooling for instant navigation + +### Path Construction Fixes +- **Breadcrumb Issues**: Fixed path calculation that was cutting off directory names +- **Tree Navigation**: Corrected file path construction to prevent "Is a directory" errors +- **Event Propagation**: Fixed Vue.js event handling using `(...args) => $emit('openFile', ...args)` pattern + +## Monaco Editor Configuration + +Monaco Editor requires special configuration for Vite/Electron environments: + +### Setup +- **Central Configuration**: `src/utils/monaco-config.ts` provides centralized worker setup +- **Worker Configuration**: Proper import of workers using Vite's `?worker` suffix +- **Applied To**: RemoteFileEditor, SQLEditor (via useSQLEditor), DotEnvEditor + +### Common Issues +- **`toUrl` Errors**: Fixed by configuring `MonacoEnvironment.getWorker` properly +- **Worker Loading**: Use `import worker from 'monaco-editor/esm/vs/.../worker?worker'` pattern +- **Multiple Components**: Always call `configureMonaco()` before using Monaco in any component + +### Vite Configuration +```typescript +optimizeDeps: { + include: ['monaco-editor'] +}, +define: { + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development') +} +``` + ## Git Integration The application includes Git integration features: From 5071d6da6757e8952eeb1ca7d4805be2d5d221eb Mon Sep 17 00:00:00 2001 From: Tiago Padilha Date: Tue, 27 May 2025 10:28:23 -0300 Subject: [PATCH 20/21] feat: update DirectoryTreeNode type to support files in the file explorer --- src/types/file-explorer.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/file-explorer.d.ts b/src/types/file-explorer.d.ts index 83c3f76..569e2cd 100644 --- a/src/types/file-explorer.d.ts +++ b/src/types/file-explorer.d.ts @@ -11,7 +11,7 @@ export interface FileEntry { export interface DirectoryTreeNode { name: string; path: string; - type: 'directory'; + type: 'directory' | 'file'; expanded?: boolean; children?: DirectoryTreeNode[]; } From e720907e8c87b413e807096c7f1b77c340a2e160 Mon Sep 17 00:00:00 2001 From: Tiago Padilha Date: Tue, 27 May 2025 10:30:10 -0300 Subject: [PATCH 21/21] feat: enhance Monaco editor configuration with custom worker implementation and fix path construction issues --- CLAUDE.md | 14 +++++-- package.json | 2 +- src/utils/monaco-config.ts | 75 ++++++++++++++++++++++++++------------ 3 files changed, 62 insertions(+), 29 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0f60c3a..21a3421 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -119,21 +119,24 @@ The application follows a communication pattern using Electron's IPC: The application includes an enhanced remote file explorer with sidebar navigation: ### Features + - **Directory Tree Sidebar**: Collapsible tree structure for fast navigation through nested directories - **State Persistence**: Remembers current directory when switching between file explorer and database views - **File Integration**: Click files in tree to open directly in Monaco Editor - **Connection-Aware State**: Different states maintained per SSH connection ### Implementation Details + - **Store**: `src/store/fileExplorer.ts` - Pinia store managing directory tree state and file operations -- **Components**: - - `DirectoryTreeSidebar.vue` - Main sidebar with expand/collapse functionality - - `DirectoryTreeNode.vue` - Individual tree nodes with file/directory icons - - `RemoteFileExplorer.vue` - Main file explorer with integrated sidebar +- **Components**: + - `DirectoryTreeSidebar.vue` - Main sidebar with expand/collapse functionality + - `DirectoryTreeNode.vue` - Individual tree nodes with file/directory icons + - `RemoteFileExplorer.vue` - Main file explorer with integrated sidebar - **State Management**: Uses `expandedPaths` Set to track opened directories and `currentPath` for navigation - **Performance**: Leverages existing optimized SSH connection pooling for instant navigation ### Path Construction Fixes + - **Breadcrumb Issues**: Fixed path calculation that was cutting off directory names - **Tree Navigation**: Corrected file path construction to prevent "Is a directory" errors - **Event Propagation**: Fixed Vue.js event handling using `(...args) => $emit('openFile', ...args)` pattern @@ -143,16 +146,19 @@ The application includes an enhanced remote file explorer with sidebar navigatio Monaco Editor requires special configuration for Vite/Electron environments: ### Setup + - **Central Configuration**: `src/utils/monaco-config.ts` provides centralized worker setup - **Worker Configuration**: Proper import of workers using Vite's `?worker` suffix - **Applied To**: RemoteFileEditor, SQLEditor (via useSQLEditor), DotEnvEditor ### Common Issues + - **`toUrl` Errors**: Fixed by configuring `MonacoEnvironment.getWorker` properly - **Worker Loading**: Use `import worker from 'monaco-editor/esm/vs/.../worker?worker'` pattern - **Multiple Components**: Always call `configureMonaco()` before using Monaco in any component ### Vite Configuration + ```typescript optimizeDeps: { include: ['monaco-editor'] diff --git a/package.json b/package.json index 36c4cb1..bc85ee5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "larabase", - "version": "1.2.5", + "version": "1.2.6", "main": "dist-electron/main/index.js", "description": "An Opined Database GUI for Laravel Developers", "author": "Tiago Padilha ", diff --git a/src/utils/monaco-config.ts b/src/utils/monaco-config.ts index 4d4232c..cdc07b3 100644 --- a/src/utils/monaco-config.ts +++ b/src/utils/monaco-config.ts @@ -1,33 +1,60 @@ -import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; -import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'; -import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'; -import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'; -import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'; - let isConfigured = false; export function configureMonaco() { if (isConfigured) return; (self as any).MonacoEnvironment = { - getWorker(_: any, label: string) { - if (label === 'json') { - return new jsonWorker(); - } - if (label === 'css' || label === 'scss' || label === 'less') { - return new cssWorker(); - } - if ( - label === 'html' || - label === 'handlebars' || - label === 'razor' - ) { - return new htmlWorker(); - } - if (label === 'typescript' || label === 'javascript') { - return new tsWorker(); - } - return new editorWorker(); + getWorker: function () { + const workerCode = ` + class MonacoWorker { + constructor() { + this.messageId = 0; + this.pendingRequests = new Map(); + } + + handleMessage(e) { + const { id, method, args } = e.data; + + try { + switch (method) { + case 'validate': + this.postMessage({ id, result: [] }); + break; + case 'format': + this.postMessage({ id, result: args[0] || '' }); + break; + case 'doHover': + this.postMessage({ id, result: null }); + break; + case 'provideCompletionItems': + this.postMessage({ id, result: { suggestions: [] } }); + break; + default: + this.postMessage({ id, result: null }); + } + } catch (error) { + this.postMessage({ id, error: error.message }); + } + } + + postMessage(message) { + self.postMessage(message); + } + } + + const worker = new MonacoWorker(); + + self.addEventListener('message', (e) => { + worker.handleMessage(e); + }); + + self.postMessage({ type: 'ready' }); + `; + + const blob = new Blob([workerCode], { + type: 'application/javascript' + }); + return new Worker(URL.createObjectURL(blob)); } };