diff --git a/rspack.config.js b/rspack.config.js index 4cf1ebf9a..c8e64e72d 100644 --- a/rspack.config.js +++ b/rspack.config.js @@ -94,10 +94,15 @@ module.exports = (env, options) => { resourceQuery: /raw/, type: 'asset/source', }, + { + test: /\.(png|svg|jpg|jpeg|ico|webp)(\?.*)?$/, + resourceQuery: /inline/, + type: 'asset/inline', + }, // Asset files { test: /\.(png|svg|jpg|jpeg|ico|ttf|webp|eot|woff|webm|mp4|wav)(\?.*)?$/, - resourceQuery: { not: [/raw/] }, + resourceQuery: { not: [/raw/, /inline/] }, type: 'asset/resource', }, // Regular CSS/SCSS files diff --git a/src/cm/lsp/clientManager.ts b/src/cm/lsp/clientManager.ts index 1720a86ac..196ef14a7 100644 --- a/src/cm/lsp/clientManager.ts +++ b/src/cm/lsp/clientManager.ts @@ -18,6 +18,7 @@ import Url from "utils/Url"; import { clearDiagnosticsEffect } from "./diagnostics"; import { supportsBuiltinFormatting } from "./formattingSupport"; import { inlayHintsExtension } from "./inlayHints"; +import { addLspLog } from "./logs"; import { acodeRenameKeymap } from "./rename"; import { selectRuntimeProvider } from "./runtimeProviders"; import serverRegistry from "./serverRegistry"; @@ -689,6 +690,7 @@ export class LspClientManager { level = "info"; } const logFn = console[level] ?? console.info; + addLspLog(server.id, level === "log" ? "info" : level, message); logFn(`[LSP:${server.id}] ${message}`); return true; }, @@ -716,6 +718,7 @@ export class LspClientManager { icon: type === 1 ? "error" : "warningreport_problem", type: type === 1 ? "error" : "warning", }); + addLspLog(server.id, type === 1 ? "error" : "warn", message); logLspInfo(`[LSP:${server.id}] ${message}`); return true; } @@ -728,6 +731,7 @@ export class LspClientManager { icon: type === 4 ? "autorenew" : "info", duration: 5000, }); + addLspLog(server.id, "info", message); logLspInfo(`[LSP:${server.id}] ${message}`); return true; }, @@ -906,6 +910,7 @@ export class LspClientManager { ); } logLspInfo(`[LSP:${server.id}] initialized`); + addLspLog(server.id, "info", "Initialized"); client.__acodeLoggedInfo = true; } } catch (error) { diff --git a/src/cm/lsp/connectionState.js b/src/cm/lsp/connectionState.js new file mode 100644 index 000000000..123665709 --- /dev/null +++ b/src/cm/lsp/connectionState.js @@ -0,0 +1,32 @@ +import serverRegistry from "./serverRegistry"; + +function getCurrentFileLanguage() { + try { + const file = window.editorManager?.activeFile; + if (!file || file.type !== "editor") return null; + return file.currentMode?.toLowerCase() || null; + } catch { + return null; + } +} + +function getServersForCurrentFile() { + const language = getCurrentFileLanguage(); + if (!language) return []; + + try { + return serverRegistry.getServersForLanguage(language); + } catch { + return []; + } +} + +function hasConnectedServers() { + return getServersForCurrentFile().length > 0; +} + +export { + getCurrentFileLanguage, + getServersForCurrentFile, + hasConnectedServers, +}; diff --git a/src/cm/lsp/index.ts b/src/cm/lsp/index.ts index 41654762b..904c6c31c 100644 --- a/src/cm/lsp/index.ts +++ b/src/cm/lsp/index.ts @@ -55,6 +55,14 @@ export { inlayHintsEditorExtension, inlayHintsExtension, } from "./inlayHints"; +export { + addLspLog, + clearLspLogs, + getLspLogs, + onLspLog, + type LspLogEntry, + type LspLogLevel, +} from "./logs"; export { closeReferencesPanel, findAllReferences, diff --git a/src/cm/lsp/logs.ts b/src/cm/lsp/logs.ts new file mode 100644 index 000000000..eb5dacd8d --- /dev/null +++ b/src/cm/lsp/logs.ts @@ -0,0 +1,88 @@ +export type LspLogLevel = "debug" | "info" | "log" | "warn" | "error" | "stderr"; + +export interface LspLogEntry { + timestamp: Date; + level: LspLogLevel; + message: string; + details?: unknown; +} + +const MAX_LOGS = 200; +const logsByServer = new Map(); +const listeners = new Set<(serverId: string, entry: LspLogEntry) => void>(); +const IGNORED_LOG_PATTERNS = [ + /\$\/progress\b/i, + /\bProgress:/i, + /\bwindow\/workDoneProgress\/create\b/i, + /\bAuto-responded to window\/workDoneProgress\/create\b/i, +]; + +function stripAnsi(value: string): string { + return value.replace(/\x1b\[[0-9;]*m/g, ""); +} + +function normalizeMessage(message: unknown): string { + let text: string; + if (typeof message === "string") { + text = message; + } else if (message instanceof Error) { + text = message.message; + } else { + try { + text = JSON.stringify(message); + } catch { + text = String(message); + } + } + return stripAnsi(String(text || "")) + .replace(/\[LSP:[^\]]+\]\s*/g, "") + .replace(/\[LSP-STDERR:[^\]]+\]\s*/g, "") + .replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?\s*/g, "") + .replace(/\s*(INFO|WARN|ERROR|DEBUG|TRACE)\s+/gi, "") + .replace(/[a-z_]+::[a-z_]+:\s*/gi, "") + .trim(); +} + +function shouldIgnoreLog(message: string): boolean { + return IGNORED_LOG_PATTERNS.some((pattern) => pattern.test(message)); +} + +export function addLspLog( + serverId: string, + level: LspLogLevel, + message: unknown, + details?: unknown, +): void { + const id = String(serverId || "").trim(); + if (!id) return; + + const normalized = normalizeMessage(message); + if (!normalized || shouldIgnoreLog(normalized)) return; + + const logs = logsByServer.get(id) || []; + const entry: LspLogEntry = { + timestamp: new Date(), + level, + message: normalized, + details, + }; + logs.push(entry); + if (logs.length > MAX_LOGS) logs.splice(0, logs.length - MAX_LOGS); + logsByServer.set(id, logs); + listeners.forEach((listener) => listener(id, entry)); +} + +export function getLspLogs(serverId: string): LspLogEntry[] { + return logsByServer.get(String(serverId || "").trim()) || []; +} + +export function clearLspLogs(serverId: string): void { + logsByServer.delete(String(serverId || "").trim()); +} + +export function onLspLog( + listener: (serverId: string, entry: LspLogEntry) => void, +): () => void { + listeners.add(listener); + return () => listeners.delete(listener); +} diff --git a/src/cm/lsp/runtimeActions.ts b/src/cm/lsp/runtimeActions.ts index 17455c9aa..2fe36af8a 100644 --- a/src/cm/lsp/runtimeActions.ts +++ b/src/cm/lsp/runtimeActions.ts @@ -9,26 +9,33 @@ import type { function getSettingsContext( server: LspServerDefinition, context: LspRuntimeContext = {}, + runtimeAction: LspRuntimeContext["runtimeAction"] = "command", ): LspRuntimeContext { return { ...context, serverId: context.serverId || server.id, allowNonTerminalWorkspace: true, + runtimeAction, }; } async function getProvider( server: LspServerDefinition, context: LspRuntimeContext = {}, + runtimeAction?: LspRuntimeContext["runtimeAction"], ): Promise { - return selectRuntimeProvider(server, getSettingsContext(server, context)); + return selectRuntimeProvider( + server, + getSettingsContext(server, context, runtimeAction), + ); } export async function checkRuntimeServerInstallation( server: LspServerDefinition, context?: LspRuntimeContext, ): Promise { - const provider = await getProvider(server, context); + const settingsContext = getSettingsContext(server, context, "checkInstallation"); + const provider = await getProvider(server, context, "checkInstallation"); if (!provider?.checkInstallation) { return { status: "unknown", @@ -38,7 +45,7 @@ export async function checkRuntimeServerInstallation( message: "The selected runtime does not provide installation checks.", }; } - return provider.checkInstallation(server, getSettingsContext(server, context)); + return provider.checkInstallation(server, settingsContext); } export async function installRuntimeServer( @@ -47,11 +54,12 @@ export async function installRuntimeServer( options: { promptConfirm?: boolean } = {}, context?: LspRuntimeContext, ): Promise { - const provider = await getProvider(server, context); + const settingsContext = getSettingsContext(server, context, "install"); + const provider = await getProvider(server, context, "install"); if (!provider?.install) { throw new Error("The selected runtime does not support installation."); } - return provider.install(server, getSettingsContext(server, context), mode, options); + return provider.install(server, settingsContext, mode, options); } export async function uninstallRuntimeServer( @@ -59,11 +67,12 @@ export async function uninstallRuntimeServer( options: { promptConfirm?: boolean } = {}, context?: LspRuntimeContext, ): Promise { - const provider = await getProvider(server, context); + const settingsContext = getSettingsContext(server, context, "uninstall"); + const provider = await getProvider(server, context, "uninstall"); if (!provider?.uninstall) { throw new Error("The selected runtime does not support uninstall."); } - return provider.uninstall(server, getSettingsContext(server, context), options); + return provider.uninstall(server, settingsContext, options); } export async function getRuntimeInstallCommand( @@ -71,11 +80,12 @@ export async function getRuntimeInstallCommand( mode: "install" | "update" = "install", context?: LspRuntimeContext, ): Promise { - const provider = await getProvider(server, context); + const settingsContext = getSettingsContext(server, context, "command"); + const provider = await getProvider(server, context, "command"); return ( provider?.getInstallCommand?.( server, - getSettingsContext(server, context), + settingsContext, mode, ) ?? null ); @@ -85,11 +95,12 @@ export async function getRuntimeUninstallCommand( server: LspServerDefinition, context?: LspRuntimeContext, ): Promise { - const provider = await getProvider(server, context); + const settingsContext = getSettingsContext(server, context, "command"); + const provider = await getProvider(server, context, "command"); return ( provider?.getUninstallCommand?.( server, - getSettingsContext(server, context), + settingsContext, ) ?? null ); } diff --git a/src/cm/lsp/runtimes/builtinAlpine.ts b/src/cm/lsp/runtimes/builtinAlpine.ts index 5e158bb6d..499ba1545 100644 --- a/src/cm/lsp/runtimes/builtinAlpine.ts +++ b/src/cm/lsp/runtimes/builtinAlpine.ts @@ -56,6 +56,10 @@ export const builtinAlpineRuntimeProvider: LspRuntimeProvider = { server: LspServerDefinition, context: LspRuntimeContext, ): boolean { + if (context.runtimeAction && server.launcher) { + return true; + } + return ( !!server.launcher && (canUseRealPath(context) || diff --git a/src/cm/lsp/runtimes/externalWebSocket.ts b/src/cm/lsp/runtimes/externalWebSocket.ts index 90659bf8b..0d0d7e6ad 100644 --- a/src/cm/lsp/runtimes/externalWebSocket.ts +++ b/src/cm/lsp/runtimes/externalWebSocket.ts @@ -17,7 +17,10 @@ export const externalWebSocketRuntimeProvider: LspRuntimeProvider = { label: "External WebSocket", priority: -50, - canHandle(server) { + canHandle(server, context) { + if (context.runtimeAction && server.launcher) { + return false; + } return server.transport?.kind === "websocket" && !!server.transport.url; }, diff --git a/src/cm/lsp/serverLauncher.ts b/src/cm/lsp/serverLauncher.ts index 87f0a1852..c7db21e10 100644 --- a/src/cm/lsp/serverLauncher.ts +++ b/src/cm/lsp/serverLauncher.ts @@ -16,6 +16,7 @@ import { } from "./runtimes/axsBridge"; import { getServerBundle } from "./serverCatalog"; import notificationManager from "lib/notificationManager"; +import { addLspLog } from "./logs"; import type { InstallCheckResult, InstallStatus, @@ -278,6 +279,7 @@ export async function canReuseExistingServer( (await checkServerAliveViaWebSocket(url, 1000))); if (alive) { + addLspLog(server.id, "info", `Reusing existing server on port ${portInfo.port}`); console.info( `[LSP:${server.id}] Reusing existing server on port ${portInfo.port}`, ); @@ -287,6 +289,7 @@ export async function canReuseExistingServer( console.info( `[LSP:${server.id}] Found stale port file, will start new server`, ); + addLspLog(server.id, "warn", "Found stale port file, starting a new server"); return null; } @@ -909,8 +912,10 @@ async function startInteractiveServer( const callback: ExecutorCallback = (type, data) => { if (type === "stderr") { if (/proot warning/i.test(data)) return; + addLspLog(serverId, "stderr", data); console.warn(`[LSP:${serverId}] ${data}`); } else if (type === "stdout" && data && data.trim()) { + addLspLog(serverId, "info", data); console.info(`[LSP:${serverId}] ${data}`); // Detect when the axs proxy signals it's listening if (/listening on/i.test(data)) { @@ -919,6 +924,7 @@ async function startInteractiveServer( } }; const uuid = await executor.start(command, callback, true); + addLspLog(serverId, "info", `Started shell process ${uuid}`); managedServers.set(serverId, { uuid, command, @@ -1115,6 +1121,7 @@ export async function ensureServerRunning( console.info( `[LSP:${server.id}] Auto-discovered port ${discoveredPort}`, ); + addLspLog(server.id, "info", `Auto-discovered port ${discoveredPort}`); // Update managed server entry with the port const entry = managedServers.get(key); if (entry) { @@ -1137,12 +1144,14 @@ export async function ensureServerRunning( if (!announcedServers.has(key)) { console.info(`[LSP:${server.id}] ${server.label} connected`); + addLspLog(server.id, "info", `${server.label} connected`); announcedServers.add(key); } return { uuid, discoveredPort }; } catch (error) { console.error(`Failed to start language server ${server.id}`, error); const errorMessage = error instanceof Error ? error.message : String(error); + addLspLog(server.id, "error", errorMessage || "Connection failed"); lspStatusBar.show({ message: errorMessage || "Connection failed", title: `${server.label} failed`, @@ -1173,6 +1182,7 @@ export async function ensureServerRunning( export function stopManagedServer(serverId: string): void { const entry = managedServers.get(serverId); if (!entry) return; + addLspLog(serverId, "info", "Stopping managed server"); const executor = getExecutor(); executor.stop(entry.uuid).catch((error: Error) => { console.warn(`Failed to stop language server ${serverId}`, error); diff --git a/src/cm/lsp/transport.ts b/src/cm/lsp/transport.ts index 3a9d0a71c..5697782b9 100644 --- a/src/cm/lsp/transport.ts +++ b/src/cm/lsp/transport.ts @@ -4,6 +4,7 @@ */ import type { Transport } from "@codemirror/lsp-client"; +import { addLspLog } from "./logs"; import type { LspServerDefinition, TransportContext, @@ -44,6 +45,7 @@ function createWebSocketTransport( console.info( `[LSP:${server.id}] Using auto-discovered port ${context.dynamicPort}`, ); + addLspLog(server.id, "info", `Using auto-discovered port ${context.dynamicPort}`); } // URL is only required when not using dynamic port @@ -175,17 +177,24 @@ function createWebSocketTransport( const wasClean = event.wasClean || event.code === 1000; if (wasClean) { console.info(`[LSP:${server.id}] WebSocket closed cleanly`); + addLspLog(server.id, "info", "WebSocket closed cleanly"); return; } console.warn( `[LSP:${server.id}] WebSocket closed unexpectedly (code: ${event.code})`, ); + addLspLog( + server.id, + "warn", + `WebSocket closed unexpectedly (code: ${event.code})`, + ); if (enableReconnect && reconnectAttempts < maxReconnectAttempts) { scheduleReconnect(); } else if (reconnectAttempts >= maxReconnectAttempts) { console.error(`[LSP:${server.id}] Max reconnection attempts reached`); + addLspLog(server.id, "error", "Max reconnection attempts reached"); } } @@ -195,6 +204,7 @@ function createWebSocketTransport( const reason = errorEvent?.message || errorEvent?.type || "connection error"; console.error(`[LSP:${server.id}] WebSocket error: ${reason}`); + addLspLog(server.id, "error", `WebSocket error: ${reason}`); } function scheduleReconnect(): void { @@ -209,6 +219,11 @@ function createWebSocketTransport( console.info( `[LSP:${server.id}] Reconnecting in ${delay}ms (attempt ${reconnectAttempts}/${maxReconnectAttempts})`, ); + addLspLog( + server.id, + "info", + `Reconnecting in ${delay}ms (attempt ${reconnectAttempts}/${maxReconnectAttempts})`, + ); reconnectTimer = setTimeout(() => { reconnectTimer = null; @@ -228,6 +243,7 @@ function createWebSocketTransport( connected = true; reconnectAttempts = 0; console.info(`[LSP:${server.id}] Reconnected successfully`); + addLspLog(server.id, "info", "Reconnected successfully"); if (socket) { socket.onopen = null; } diff --git a/src/cm/lsp/types.ts b/src/cm/lsp/types.ts index 98174106f..1ea75b204 100644 --- a/src/cm/lsp/types.ts +++ b/src/cm/lsp/types.ts @@ -103,6 +103,7 @@ export interface LspRuntimeContext extends TransportContext { serverId?: string; workspaceKind?: WorkspaceKind; allowNonTerminalWorkspace?: boolean; + runtimeAction?: "checkInstallation" | "install" | "uninstall" | "command"; } export type LspClientScope = "workspace" | "document"; diff --git a/src/components/logo/style.scss b/src/components/logo/style.scss index 90f9f8a3c..01568da03 100644 --- a/src/components/logo/style.scss +++ b/src/components/logo/style.scss @@ -6,7 +6,7 @@ margin: 0 auto; &::after { - content: ''; + content: ""; position: absolute; top: 0; left: 0; @@ -19,13 +19,19 @@ } &::before { - content: ''; + content: ""; position: absolute; top: 0; left: 0; width: 100%; height: 100%; - background: radial-gradient(circle, rgb(68, 153, 254, 0.5), rgb(68, 153, 254, 0.1), rgb(68, 153, 254, 0), rgb(68, 153, 254, 0)); + background: radial-gradient( + circle, + rgb(68, 153, 254, 0.5), + rgb(68, 153, 254, 0.1), + rgb(68, 153, 254, 0), + rgb(68, 153, 254, 0) + ); overflow: visible; } -} \ No newline at end of file +} diff --git a/src/components/lspInfoDialog/index.js b/src/components/lspInfoDialog/index.js index 31b57b381..c6e0afeb4 100644 --- a/src/components/lspInfoDialog/index.js +++ b/src/components/lspInfoDialog/index.js @@ -1,143 +1,18 @@ import "./styles.scss"; import lspClientManager from "cm/lsp/clientManager"; +import { + getCurrentFileLanguage, + getServersForCurrentFile, + hasConnectedServers, +} from "cm/lsp/connectionState"; +import { addLspLog, clearLspLogs, getLspLogs } from "cm/lsp/logs"; import { getServerStats } from "cm/lsp/serverLauncher"; -import serverRegistry from "cm/lsp/serverRegistry"; import toast from "components/toast"; import actionStack from "lib/actionStack"; import restoreTheme from "lib/restoreTheme"; let dialogInstance = null; -const lspLogs = new Map(); -const MAX_LOGS = 200; -const logListeners = new Set(); -const IGNORED_LOG_PATTERNS = [ - /\$\/progress\b/i, - /\bProgress:/i, - /\bwindow\/workDoneProgress\/create\b/i, - /\bAuto-responded to window\/workDoneProgress\/create\b/i, -]; - -function shouldIgnoreLog(message) { - if (typeof message !== "string") return false; - return IGNORED_LOG_PATTERNS.some((pattern) => pattern.test(message)); -} - -function addLspLog(serverId, level, message, details = null) { - if (shouldIgnoreLog(message)) { - return; - } - - if (!lspLogs.has(serverId)) { - lspLogs.set(serverId, []); - } - const logs = lspLogs.get(serverId); - const entry = { - timestamp: new Date(), - level, - message, - details, - }; - logs.push(entry); - if (logs.length > MAX_LOGS) { - logs.shift(); - } - logListeners.forEach((fn) => fn(serverId, entry)); -} - -function getLspLogs(serverId) { - return lspLogs.get(serverId) || []; -} - -function clearLspLogs(serverId) { - lspLogs.delete(serverId); -} - -const originalConsoleInfo = console.info; -const originalConsoleWarn = console.warn; -const originalConsoleError = console.error; - -function stripAnsi(str) { - if (typeof str !== "string") return str; - return str.replace(/\x1b\[[0-9;]*m/g, ""); -} - -function extractServerId(message) { - const cleaned = stripAnsi(message); - // Match [LSP:serverId] format - const lspMatch = cleaned?.match?.(/\[LSP:([^\]]+)\]/); - if (lspMatch) return lspMatch[1]; - - // Match [LSP-STDERR:program] format from axs proxy - const stderrMatch = cleaned?.match?.(/\[LSP-STDERR:([^\]]+)\]/); - if (stderrMatch) { - const program = stderrMatch[1]; - return program; - } - - return null; -} - -function extractLogMessage(message) { - const cleaned = stripAnsi(message); - // Strip [LSP:...] and [LSP-STDERR:...] prefixes - // Strip ISO timestamps like 2026-02-05T08:26:24.745443Z - // Strip log levels like INFO, WARN, ERROR and the source like axs::lsp: - return ( - cleaned - ?.replace?.(/\[LSP:[^\]]+\]\s*/, "") - ?.replace?.(/\[LSP-STDERR:[^\]]+\]\s*/, "") - ?.replace?.(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?\s*/g, "") - ?.replace?.(/\s*(INFO|WARN|ERROR|DEBUG|TRACE)\s+/gi, "") - ?.replace?.(/[a-z_]+::[a-z_]+:\s*/gi, "") - ?.trim() || cleaned - ); -} - -console.info = function (...args) { - originalConsoleInfo.apply(console, args); - const msg = args[0]; - if ( - typeof msg === "string" && - (msg.includes("[LSP:") || msg.includes("[LSP-STDERR:")) - ) { - const serverId = extractServerId(msg); - if (serverId) { - addLspLog(serverId, "info", extractLogMessage(msg)); - } - } -}; - -console.warn = function (...args) { - originalConsoleWarn.apply(console, args); - const msg = args[0]; - if ( - typeof msg === "string" && - (msg.includes("[LSP:") || msg.includes("[LSP-STDERR:")) - ) { - const serverId = extractServerId(msg); - if (serverId) { - // stderr from axs is logged as warn, mark it appropriately - const isStderr = msg.includes("[LSP-STDERR:"); - addLspLog(serverId, isStderr ? "stderr" : "warn", extractLogMessage(msg)); - } - } -}; - -console.error = function (...args) { - originalConsoleError.apply(console, args); - const msg = args[0]; - if ( - typeof msg === "string" && - (msg.includes("[LSP:") || msg.includes("[LSP-STDERR:")) - ) { - const serverId = extractServerId(msg); - if (serverId) { - addLspLog(serverId, "error", extractLogMessage(msg)); - } - } -}; - function getActiveClients() { try { return lspClientManager.getActiveClients(); @@ -146,27 +21,6 @@ function getActiveClients() { } } -function getCurrentFileLanguage() { - try { - const file = window.editorManager?.activeFile; - if (!file || file.type !== "editor") return null; - return file.currentMode?.toLowerCase() || null; - } catch { - return null; - } -} - -function getServersForCurrentFile() { - const language = getCurrentFileLanguage(); - if (!language) return []; - - try { - return serverRegistry.getServersForLanguage(language); - } catch { - return []; - } -} - function getServerStatus(serverId) { const activeClients = getActiveClients(); const client = activeClients.find((c) => c.server?.id === serverId); @@ -693,10 +547,5 @@ function showLspInfoDialog() { } } -function hasConnectedServers() { - const relevantServers = getServersForCurrentFile(); - return relevantServers.length > 0; -} - export { addLspLog, getLspLogs, hasConnectedServers, showLspInfoDialog }; export default showLspInfoDialog; diff --git a/src/components/quickTools/footer.js b/src/components/quickTools/footer.js index 0ea0a0aef..68460f013 100644 --- a/src/components/quickTools/footer.js +++ b/src/components/quickTools/footer.js @@ -70,7 +70,7 @@ export const $footer =
; /**@type {HTMLElement} */ export const $toggler = ( ); diff --git a/src/dialogs/style.scss b/src/dialogs/style.scss index 2db23f2bb..d6a6f14cd 100644 --- a/src/dialogs/style.scss +++ b/src/dialogs/style.scss @@ -104,12 +104,12 @@ } } - &+.mask { + & + .mask { z-index: 111; background-color: rgb(0, 0, 0); opacity: 0.4; - ~.mask { + ~ .mask { opacity: 0; } } @@ -119,7 +119,7 @@ transform: translate(-50%, -50%) scale(0.95) translateZ(0); opacity: 0; - &+.mask { + & + .mask { opacity: 0; } } @@ -212,7 +212,10 @@ user-select: none; color: var(--popup-text-color, #333); opacity: 0.5; - transition: opacity 0.15s, background-color 0.15s, color 0.15s; + transition: + opacity 0.15s, + background-color 0.15s, + color 0.15s; &:hover { opacity: 0.8; @@ -283,13 +286,13 @@ align-items: center; .loader { - width: 30px; - height: 30px; + width: 24px; + height: 24px; color: rgb(153, 153, 255); - color: var(--primary-color); + color: var(--popup-active-color); display: flex; flex-shrink: 0; - margin: 0 10px; + margin-left: 10px; svg { width: 100%; diff --git a/src/lib/acode.js b/src/lib/acode.js index da96ae52e..cdc28e208 100644 --- a/src/lib/acode.js +++ b/src/lib/acode.js @@ -47,7 +47,6 @@ import prompt from "dialogs/prompt"; import select from "dialogs/select"; import { addIntentHandler, removeIntentHandler } from "handlers/intent"; import keyboardHandler from "handlers/keyboard"; -import purchaseListener from "handlers/purchase"; import windowResize from "handlers/windowResize"; import actionStack from "lib/actionStack"; import commands from "lib/commands"; @@ -67,7 +66,6 @@ import projects from "lib/projects"; import selectionMenu from "lib/selectionMenu"; import appSettings from "lib/settings"; import FileBrowser from "pages/fileBrowser"; -import formatterSettings from "settings/formatterSettings"; import ThemeBuilder from "theme/builder"; import themes from "theme/list"; import Color from "utils/color"; @@ -582,20 +580,25 @@ class Acode { if (isPaid && !purchaseToken) { if (!product) throw new Error("Product not found"); - return helpers.checkAPIStatus().then((apiStatus) => { - if (!apiStatus) { - alert(strings.error, strings.api_error); - return; - } - - iap.setPurchaseUpdatedListener( - ...purchaseListener(onpurchase, onerror), - ); - return helpers.promisify( - iap.purchase, - product.productId, - ); - }); + return helpers + .checkAPIStatus() + .then(async (apiStatus) => { + if (!apiStatus) { + alert(strings.error, strings.api_error); + return; + } + + const { default: purchaseListener } = await import( + /* webpackChunkName: "purchaseHandler" */ "handlers/purchase" + ); + iap.setPurchaseUpdatedListener( + ...purchaseListener(onpurchase, onerror), + ); + return helpers.promisify( + iap.purchase, + product.productId, + ); + }); } }) .then(() => { @@ -792,6 +795,9 @@ class Acode { } if (selectIfNull) { + const { default: formatterSettings } = await import( + /* webpackChunkName: "formatterSettings" */ "settings/formatterSettings" + ); formatterSettings(modeName); this.#afterSelectFormatter(modeName); } else { diff --git a/src/lib/commands.js b/src/lib/commands.js index 9c7aa21b6..2b2a1cc80 100644 --- a/src/lib/commands.js +++ b/src/lib/commands.js @@ -1,33 +1,18 @@ import fsOperation from "fileSystem"; import { selectAll } from "@codemirror/commands"; import Sidebar from "components/sidebar"; -import { TerminalManager } from "components/terminal"; -import color from "dialogs/color"; import confirm from "dialogs/confirm"; import prompt from "dialogs/prompt"; import select from "dialogs/select"; import actions from "handlers/quickTools"; import recents from "lib/recents"; -import About from "pages/about"; -import FileBrowser from "pages/fileBrowser"; -import plugins from "pages/plugins"; -import Problems from "pages/problems/problems"; -import openWelcomeTab from "pages/welcome/welcome"; -import changeEncoding from "palettes/changeEncoding"; -import changeMode from "palettes/changeMode"; -import changeTheme from "palettes/changeTheme"; -import commandPalette from "palettes/commandPalette"; -import findFile from "palettes/findFile"; -import browser from "plugins/browser"; -import help from "settings/helpSettings"; -import mainSettings from "settings/mainSettings"; -import { runAllTests } from "test/tester"; import { getColorRange } from "utils/color/regex"; import helpers from "utils/helpers"; import Url from "utils/Url"; import checkFiles from "./checkFiles"; import config from "./config"; import EditorFile from "./editorFile"; +import { loadFileBrowser } from "./lazyImports"; import openFile from "./openFile"; import openFolder from "./openFolder"; import run from "./run"; @@ -140,6 +125,9 @@ async function closeTabs(files, options = {}) { export default { async "run-tests"() { + const { runAllTests } = await import( + /* webpackChunkName: "tester" */ "test/tester" + ); await runAllTests(); }, async "close-all-tabs"() { @@ -187,7 +175,10 @@ export default { if (!appSettings.value.checkFiles) return; checkFiles(); }, - "command-palette"() { + async "command-palette"() { + const { default: commandPalette } = await import( + /* webpackChunkName: "commandPalette" */ "palettes/commandPalette" + ); commandPalette(); }, "disable-fullscreen"() { @@ -198,7 +189,10 @@ export default { app.classList.add("fullscreen-mode"); this["resize-editor"](); }, - encoding() { + async encoding() { + const { default: changeEncoding } = await import( + /* webpackChunkName: "changeEncoding" */ "palettes/changeEncoding" + ); changeEncoding(); }, exit() { @@ -207,10 +201,14 @@ export default { "edit-with"() { editorManager.activeFile.editWith(); }, - "find-file"() { + async "find-file"() { + const { default: findFile } = await import( + /* webpackChunkName: "findFile" */ "palettes/findFile" + ); findFile(); }, - files() { + async files() { + const FileBrowser = await loadFileBrowser(); FileBrowser("both", strings["file browser"]) .then(FileBrowser.open) .catch(FileBrowser.openError); @@ -256,30 +254,44 @@ export default { editorManager.files[fileIndex].makeActive(); }, - open(page) { + async open(page) { switch (page) { case "settings": - mainSettings(); + ( + await import( + /* webpackChunkName: "mainSettings" */ "settings/mainSettings" + ) + ).default(); break; case "help": - help(); + ( + await import( + /* webpackChunkName: "helpSettings" */ "settings/helpSettings" + ) + ).default(); break; case "problems": - Problems(); + ( + await import( + /* webpackChunkName: "problems" */ "pages/problems/problems" + ) + ).default(); break; case "plugins": - plugins(); + ( + await import(/* webpackChunkName: "plugins" */ "pages/plugins") + ).default(); break; case "file_browser": - FileBrowser(); + (await loadFileBrowser())(); break; case "about": - About(); + (await import(/* webpackChunkName: "about" */ "pages/about")).default(); break; default: @@ -290,14 +302,16 @@ export default { "open-with"() { editorManager.activeFile.openWith(); }, - "open-file"() { + async "open-file"() { editorManager.editor.contentDOM.blur(); + const FileBrowser = await loadFileBrowser(); FileBrowser("file") .then(FileBrowser.openFile) .catch(FileBrowser.openFileError); }, - "open-folder"() { + async "open-folder"() { editorManager.editor.contentDOM.blur(); + const FileBrowser = await loadFileBrowser(); FileBrowser("folder") .then(FileBrowser.openFolder) .catch(FileBrowser.openFolderError); @@ -338,7 +352,10 @@ export default { // TODO : Codemirror //editorManager.editor.resize(true); }, - "open-inapp-browser"(url) { + async "open-inapp-browser"(url) { + const { default: browser } = await import( + /* webpackChunkName: "browserPlugin" */ "plugins/browser" + ); browser.open(url); }, run() { @@ -433,13 +450,22 @@ export default { helpers.error(error); } }, - syntax() { + async syntax() { + const { default: changeMode } = await import( + /* webpackChunkName: "changeMode" */ "palettes/changeMode" + ); changeMode(); }, - "change-app-theme"() { + async "change-app-theme"() { + const { default: changeTheme } = await import( + /* webpackChunkName: "changeTheme" */ "palettes/changeTheme" + ); changeTheme("app"); }, - "change-editor-theme"() { + async "change-editor-theme"() { + const { default: changeTheme } = await import( + /* webpackChunkName: "changeTheme" */ "palettes/changeTheme" + ); changeTheme("editor"); }, "toggle-fullscreen"() { @@ -472,6 +498,9 @@ export default { const wasFocused = editorManager.activeFile.focused; let res; try { + const { default: color } = await import( + /* webpackChunkName: "colorDialog" */ "dialogs/color" + ); res = await color(defaultColor, () => { if (wasFocused) { editor.focus(); @@ -637,6 +666,9 @@ Additional Info: }, async "new-terminal"() { try { + const { TerminalManager } = await import( + /* webpackChunkName: "terminal" */ "components/terminal" + ); await TerminalManager.createServerTerminal(); } catch (error) { console.error("Failed to create terminal:", error); @@ -649,7 +681,10 @@ Additional Info: ); RunningProcesses(); }, - welcome() { + async welcome() { + const { default: openWelcomeTab } = await import( + /* webpackChunkName: "welcome" */ "pages/welcome/welcome" + ); openWelcomeTab(); }, async "toggle-inspector"() { diff --git a/src/lib/languageModeRecommendations.js b/src/lib/languageModeRecommendations.js index 48143e177..d534e2bf1 100644 --- a/src/lib/languageModeRecommendations.js +++ b/src/lib/languageModeRecommendations.js @@ -11,15 +11,16 @@ function withSupportedEditor(url) { return `${url}${separator}supported_editor=${config.SUPPORTED_EDITOR}`; } -function getSearchKeyword(filename) { - const ext = Path.extname(filename || "") - .replace(/^\./, "") +export function getLanguageModeRecommendationSearchKeyword(filename) { + const basename = Path.basename(filename || "") .trim() .toLowerCase(); + const ext = Path.extname(basename).replace(/^\./, "").trim().toLowerCase(); + const keyword = ext || (basename.startsWith(".") ? basename.slice(1) : ""); - if (!/^[a-z0-9][a-z0-9._+-]*$/.test(ext)) return ""; + if (!/^[a-z0-9][a-z0-9._+-]*$/.test(keyword)) return ""; - return ext; + return keyword; } function getIssueUrl(keyword) { @@ -89,7 +90,7 @@ class LanguageModeRecommendations { const filename = file.filename || ""; if (!hasPlainTextFallback(modeInfo, filename)) return; - const keyword = getSearchKeyword(filename); + const keyword = getLanguageModeRecommendationSearchKeyword(filename); if ( !keyword || this.notifiedKeywords.has(keyword) || diff --git a/src/lib/lazyImports.js b/src/lib/lazyImports.js new file mode 100644 index 000000000..d296f1a9e --- /dev/null +++ b/src/lib/lazyImports.js @@ -0,0 +1,5 @@ +export async function loadFileBrowser() { + return ( + await import(/* webpackChunkName: "fileBrowser" */ "pages/fileBrowser") + ).default; +} diff --git a/src/lib/openFolder.js b/src/lib/openFolder.js index 6b96c74f9..21e21b4f8 100644 --- a/src/lib/openFolder.js +++ b/src/lib/openFolder.js @@ -3,7 +3,6 @@ import sidebarApps from "sidebarApps"; import collapsableList from "components/collapsableList"; import FileTree from "components/fileTree"; import Sidebar from "components/sidebar"; -import { TerminalManager } from "components/terminal"; import tile from "components/tile"; import toast from "components/toast"; import alert from "dialogs/alert"; @@ -11,13 +10,13 @@ import confirm from "dialogs/confirm"; import prompt from "dialogs/prompt"; import select from "dialogs/select"; import escapeStringRegexp from "escape-string-regexp"; -import FileBrowser from "pages/fileBrowser"; import helpers from "utils/helpers"; import Path from "utils/Path"; import Uri from "utils/Uri"; import Url from "utils/Url"; import config from "./config"; import * as FileList from "./fileList"; +import { loadFileBrowser } from "./lazyImports"; import openFile from "./openFile"; import recents from "./recents"; import appSettings from "./settings"; @@ -184,22 +183,22 @@ function openFolder(_path, opts = {}) { }, }; + if (typeof listFiles !== "boolean") { + listFiles = appSettings.value.fileBrowser?.listFiles ?? true; + } + + folder.listFiles = listFiles; + addedFolder.push(folder); + editorManager.emit("update", "add-folder"); editorManager.onupdate("add-folder", event); editorManager.emit("add-folder", event); - (async () => { - if (typeof listFiles !== "boolean") { - listFiles = appSettings.value.fileBrowser?.listFiles ?? true; - } - - if (listFiles) { - FileList.addRoot({ url: _path, name: title }); - } - - folder.listFiles = listFiles; - addedFolder.push(folder); - })(); + if (listFiles) { + FileList.addRoot({ url: _path, name: title }).catch((err) => { + console.error("Failed to add root to FileList:", err); + }); + } if (listState[_path]) { $root.expand(); @@ -539,6 +538,9 @@ function execOperation(type, action, url, $target, name) { async function openInTerminal() { try { + const { TerminalManager } = await import( + /* webpackChunkName: "terminal" */ "components/terminal" + ); const prootPath = convertToProotPath(url); const terminal = await TerminalManager.createTerminal({ name: `Terminal - ${name}`, @@ -897,6 +899,7 @@ function execOperation(type, action, url, $target, name) { async function insertFile() { startLoading(); try { + const FileBrowser = await loadFileBrowser(); const file = await FileBrowser("file", strings["insert file"]); const sourceFs = fsOperation(file.url); const data = await sourceFs.readFile(); @@ -923,6 +926,7 @@ function execOperation(type, action, url, $target, name) { } async function open() { + const FileBrowser = await loadFileBrowser(); FileBrowser.openFolder({ url, name, diff --git a/src/lib/prettierFormatter.js b/src/lib/prettierFormatter.js index 4b93fa0f6..fb1a3b86c 100644 --- a/src/lib/prettierFormatter.js +++ b/src/lib/prettierFormatter.js @@ -14,8 +14,6 @@ import prettier from "prettier/standalone"; import helpers from "utils/helpers"; import Url from "utils/Url"; -const PRETTIER_ID = "prettier"; -const PRETTIER_NAME = "Prettier"; const CONFIG_FILENAMES = [ ".prettierrc", ".prettierrc.json", @@ -72,48 +70,7 @@ const MODE_TO_PARSER = { javascript: "babel", }; -const SUPPORTED_EXTENSIONS = [ - "js", - "cjs", - "mjs", - "jsx", - "ts", - "tsx", - "json", - "json5", - "css", - "scss", - "less", - "html", - "htm", - "vue", - "md", - "markdown", - "mdx", - "yaml", - "yml", - "graphql", - "gql", -]; - -/** - * Register Prettier formatter with Acode instance - */ -export function registerPrettierFormatter() { - if (!window?.acode) return; - const alreadyRegistered = acode.formatters.some( - ({ id }) => id === PRETTIER_ID, - ); - if (alreadyRegistered) return; - acode.registerFormatter( - PRETTIER_ID, - SUPPORTED_EXTENSIONS, - () => formatActiveFileWithPrettier(), - PRETTIER_NAME, - ); -} - -async function formatActiveFileWithPrettier() { +export async function formatActiveFileWithPrettier() { const file = editorManager?.activeFile; const editor = editorManager?.editor; if (!file || file.type !== "editor" || !editor) return false; diff --git a/src/lib/registerPrettierFormatter.js b/src/lib/registerPrettierFormatter.js new file mode 100644 index 000000000..b89fb1d1c --- /dev/null +++ b/src/lib/registerPrettierFormatter.js @@ -0,0 +1,46 @@ +const PRETTIER_ID = "prettier"; +const PRETTIER_NAME = "Prettier"; + +const SUPPORTED_EXTENSIONS = [ + "js", + "cjs", + "mjs", + "jsx", + "ts", + "tsx", + "json", + "json5", + "css", + "scss", + "less", + "html", + "htm", + "vue", + "md", + "markdown", + "mdx", + "yaml", + "yml", + "graphql", + "gql", +]; + +export function registerPrettierFormatter() { + if (!window?.acode) return; + const alreadyRegistered = acode.formatters.some( + ({ id }) => id === PRETTIER_ID, + ); + if (alreadyRegistered) return; + + acode.registerFormatter( + PRETTIER_ID, + SUPPORTED_EXTENSIONS, + async () => { + const { formatActiveFileWithPrettier } = await import( + /* webpackChunkName: "prettierFormatter" */ "lib/prettierFormatter" + ); + return formatActiveFileWithPrettier(); + }, + PRETTIER_NAME, + ); +} diff --git a/src/main.js b/src/main.js index bc90e0f86..bb285801d 100644 --- a/src/main.js +++ b/src/main.js @@ -6,6 +6,8 @@ import "res/icons/style.css"; import "res/file-icons/style.css"; import "styles/overrideAceStyle.scss"; import "styles/wideScreen.scss"; +// Editor tabs use a shadow root that only links build/main.css. +import "pages/welcome/welcome.scss"; import "lib/polyfill"; import "cm/supportedModes"; @@ -15,6 +17,7 @@ import "handlers/editorWorkaround"; import fsOperation from "fileSystem"; import sidebarApps from "sidebarApps"; import { setKeyBindings } from "cm/commandRegistry"; +import { hasConnectedServers } from "cm/lsp/connectionState"; import { getModeForPath, getModes, @@ -22,12 +25,9 @@ import { initModes, } from "cm/modelist"; import Contextmenu from "components/contextmenu"; -import { hasConnectedServers } from "components/lspInfoDialog"; import Sidebar from "components/sidebar"; -import { TerminalManager } from "components/terminal"; import tile from "components/tile"; import toast from "components/toast"; -import tutorial from "components/tutorial"; import confirm from "dialogs/confirm"; import intentHandler, { processPendingIntents } from "handlers/intent"; import keyboardHandler, { keydownState } from "handlers/keyboard"; @@ -39,7 +39,6 @@ import adRewards from "lib/adRewards"; import ajax from "lib/ajax"; import applySettings from "lib/applySettings"; import checkFiles from "lib/checkFiles"; -import checkPluginsUpdate from "lib/checkPluginsUpdate"; import { canSaveFile } from "lib/commands"; import config from "lib/config"; import EditorFile from "lib/editorFile"; @@ -51,14 +50,11 @@ import loadPlugins from "lib/loadPlugins"; import Logger from "lib/logger"; import notificationManager from "lib/notificationManager"; import openFolder, { addedFolder } from "lib/openFolder"; -import { registerPrettierFormatter } from "lib/prettierFormatter"; +import { registerPrettierFormatter } from "lib/registerPrettierFormatter"; import restoreFiles from "lib/restoreFiles"; import settings from "lib/settings"; import startAd, { hideAd } from "lib/startAd"; import mustache from "mustache"; -import plugins from "pages/plugins"; -import openWelcomeTab from "pages/welcome"; -import otherSettings from "settings/appSettings"; import themes from "theme/list"; import { initHighlighting } from "utils/codeHighlight"; import { getEncoding, initEncodings } from "utils/encodings"; @@ -398,6 +394,9 @@ async function onDeviceReady() { }, ); } + const { default: checkPluginsUpdate } = await import( + /* webpackChunkName: "checkPluginsUpdate" */ "lib/checkPluginsUpdate" + ); checkPluginsUpdate() .then((updates) => { if (!updates.length) return; @@ -406,7 +405,10 @@ async function onDeviceReady() { `${updates.length} plugin${updates.length > 1 ? "s" : ""} ${updates.length > 1 ? "have" : "has"} new version${updates.length > 1 ? "s" : ""} available.`, { icon: "extension", - action: () => { + action: async () => { + const { default: plugins } = await import( + /* webpackChunkName: "plugins" */ "pages/plugins" + ); plugins(updates); }, }, @@ -635,6 +637,9 @@ async function loadApp() { window.log("info", "Started app and its services..."); if (!files.length) { + const { default: openWelcomeTab } = await import( + /* webpackChunkName: "welcome" */ "pages/welcome" + ); openWelcomeTab(); } @@ -676,11 +681,19 @@ async function loadApp() { onEditorUpdate(undefined, false); } + acode.exec("save-state"); initFileList(); - TerminalManager.restorePersistedSessions().catch((error) => { - console.error("Terminal restoration failed:", error); - }); + import(/* webpackChunkName: "terminal" */ "components/terminal").then( + ({ TerminalManager }) => { + TerminalManager.restorePersistedSessions().catch((error) => { + console.error("Terminal restoration failed:", error); + }); + }, + (error) => { + console.error("Failed to load terminal module:", error); + }, + ); /** * @@ -721,7 +734,9 @@ async function loadApp() { return; } - if (saveState) acode.exec("save-state"); + if (saveState && sessionStorage.getItem("isfilesRestored") === "true") { + acode.exec("save-state"); + } } async function onFileUpdate() { @@ -844,8 +859,13 @@ function createFileMenu({ top, bottom, toggler }) { return $menu; } -function showTutorials() { +async function showTutorials() { if (window.innerWidth > 750) { + const [{ default: tutorial }, { default: otherSettings }] = + await Promise.all([ + import(/* webpackChunkName: "tutorial" */ "components/tutorial"), + import(/* webpackChunkName: "appSettings" */ "settings/appSettings"), + ]); tutorial("quicktools-tutorials", (hide) => { const onclick = () => { otherSettings(); diff --git a/src/pages/welcome/welcome.js b/src/pages/welcome/welcome.js index 0ede6cf44..6bccb37a5 100644 --- a/src/pages/welcome/welcome.js +++ b/src/pages/welcome/welcome.js @@ -1,6 +1,5 @@ -import "./welcome.scss"; import { getResolvedKeyBindings } from "cm/commandRegistry"; -import Logo from "components/logo"; +import logoSrc from "components/logo/logo.png?inline"; import config from "lib/config"; import EditorFile from "lib/editorFile"; @@ -45,7 +44,7 @@ function createWelcomeContent() {
{/* Hero Section */}
- +

Welcome to Acode

Powerful code editor for Android

diff --git a/src/pages/welcome/welcome.scss b/src/pages/welcome/welcome.scss index 5a1a95840..36e7f2c98 100644 --- a/src/pages/welcome/welcome.scss +++ b/src/pages/welcome/welcome.scss @@ -1,232 +1,242 @@ #welcome-tab { + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + min-height: 100%; + height: auto; + padding: 32px 24px; + overflow-y: auto; + overflow-x: hidden; + background-color: var(--secondary-color); + + // Hero Header + .welcome-header { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 48px; + + .logo { + width: 48px; + height: auto; + max-height: 48px; + flex-shrink: 0; + } + + .welcome-header-text { + h1 { + font-size: 20px; + font-weight: 600; + color: var(--primary-text-color); + margin: 0 0 4px 0; + letter-spacing: -0.3px; + } + + .tagline { + font-size: 13px; + color: color-mix(in srgb, var(--secondary-text-color) 60%, transparent); + margin: 0; + } + } + } + + // Section Styles + .welcome-section { + width: 100%; + max-width: 400px; + margin-bottom: 32px; + + .section-label { + font-size: 11px; + font-weight: 600; + letter-spacing: 1px; + color: color-mix(in srgb, var(--secondary-text-color) 50%, transparent); + margin: 0 0 12px 0; + padding-left: 4px; + } + } + + // Action List + .action-list { display: flex; flex-direction: column; + gap: 2px; + } + + .action-row { + display: flex; align-items: center; - justify-content: flex-start; - min-height: 100%; - height: auto; - padding: 32px 24px; - overflow-y: auto; - overflow-x: hidden; - background-color: var(--secondary-color); - - // Hero Header - .welcome-header { - display: flex; - align-items: center; - gap: 16px; - margin-bottom: 48px; - - .logo { - width: 64px; - height: 64px; - flex-shrink: 0; - - &::after { - background-size: 48px; - } - - &::before { - background: radial-gradient(circle, - color-mix(in srgb, var(--button-background-color) 30%, transparent) 0%, - transparent 70%); - } - } - - .welcome-header-text { - h1 { - font-size: 20px; - font-weight: 600; - color: var(--primary-text-color); - margin: 0 0 4px 0; - letter-spacing: -0.3px; - } - - .tagline { - font-size: 13px; - color: color-mix(in srgb, var(--secondary-text-color) 60%, transparent); - margin: 0; - } - } + gap: 12px; + padding: 10px 12px; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.15s ease; + + &:hover, + &:active { + background-color: color-mix( + in srgb, + var(--popup-background-color) 40%, + transparent + ); } - // Section Styles - .welcome-section { - width: 100%; - max-width: 400px; - margin-bottom: 32px; - - .section-label { - font-size: 11px; - font-weight: 600; - letter-spacing: 1px; - color: color-mix(in srgb, var(--secondary-text-color) 50%, transparent); - margin: 0 0 12px 0; - padding-left: 4px; - } + .icon { + font-size: 16px; + color: color-mix(in srgb, var(--secondary-text-color) 70%, transparent); + width: 20px; + text-align: center; } - // Action List - .action-list { - display: flex; - flex-direction: column; - gap: 2px; + .action-label { + flex: 1; + font-size: 14px; + color: var(--secondary-text-color); } - .action-row { - display: flex; - align-items: center; - gap: 12px; - padding: 10px 12px; - border-radius: 6px; - cursor: pointer; - transition: background-color 0.15s ease; - - &:hover, - &:active { - background-color: color-mix(in srgb, var(--popup-background-color) 40%, transparent); - } - - .icon { - font-size: 16px; - color: color-mix(in srgb, var(--secondary-text-color) 70%, transparent); - width: 20px; - text-align: center; - } - - .action-label { - flex: 1; - font-size: 14px; - color: var(--secondary-text-color); - } - - .action-shortcut { - font-size: 12px; - font-family: 'Roboto Mono', monospace; - color: color-mix(in srgb, var(--secondary-text-color) 40%, transparent); - letter-spacing: 0.5px; - } + .action-shortcut { + font-size: 12px; + font-family: "Roboto Mono", monospace; + color: color-mix(in srgb, var(--secondary-text-color) 40%, transparent); + letter-spacing: 0.5px; + } + } + + // Links Section + .welcome-links { + margin-top: 16px; + + .link-row { + display: flex; + flex-wrap: wrap; + gap: 8px; + justify-content: flex-start; } - // Links Section - .welcome-links { - margin-top: 16px; - - .link-row { - display: flex; - flex-wrap: wrap; - gap: 8px; - justify-content: flex-start; - } - - .link-item { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 10px 16px; - border-radius: 8px; - text-decoration: none; - color: var(--secondary-text-color); - font-size: 13px; - font-weight: 500; - transition: all 0.15s ease; - background-color: color-mix(in srgb, var(--popup-background-color) 25%, transparent); - border: 1px solid color-mix(in srgb, var(--border-color) 20%, transparent); - - &:hover, - &:active { - background-color: color-mix(in srgb, var(--popup-background-color) 50%, transparent); - border-color: color-mix(in srgb, var(--border-color) 40%, transparent); - transform: translateY(-1px); - } - - .icon { - font-size: 16px; - color: color-mix(in srgb, var(--secondary-text-color) 80%, transparent); - } - } + .link-item { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 10px 16px; + border-radius: 8px; + text-decoration: none; + color: var(--secondary-text-color); + font-size: 13px; + font-weight: 500; + transition: all 0.15s ease; + background-color: color-mix( + in srgb, + var(--popup-background-color) 25%, + transparent + ); + border: 1px solid color-mix(in srgb, var(--border-color) 20%, transparent); + + &:hover, + &:active { + background-color: color-mix( + in srgb, + var(--popup-background-color) 50%, + transparent + ); + border-color: color-mix(in srgb, var(--border-color) 40%, transparent); + transform: translateY(-1px); + } + + .icon { + font-size: 16px; + color: color-mix(in srgb, var(--secondary-text-color) 80%, transparent); + } } + } } - @supports not (gap: 1px) { - .welcome-header > * + * { margin-left: 16px; } - .action-list > * + * { margin-top: 2px; } - .action-row > * + * { margin-left: 12px; } - .link-row > * { margin-right: 8px; margin-bottom: 8px; } - .link-item > * + * { margin-left: 8px; } +@supports not (gap: 1px) { + .welcome-header > * + * { + margin-left: 16px; + } + .action-list > * + * { + margin-top: 2px; + } + .action-row > * + * { + margin-left: 12px; + } + .link-row > * { + margin-right: 8px; + margin-bottom: 8px; } + .link-item > * + * { + margin-left: 8px; + } +} // Responsive adjustments for smaller screens @media (max-width: 360px) { - #welcome-tab { - padding: 24px 16px; - - .welcome-header { - flex-direction: column; - text-align: center; - margin-bottom: 36px; - - .logo { - width: 56px; - height: 56px; - - &::after { - background-size: 40px; - } - } - - .welcome-header-text h1 { - font-size: 18px; - } - } - - .welcome-section { - margin-bottom: 24px; - } - - .action-row { - padding: 8px 10px; - - .action-label { - font-size: 13px; - } - - .action-shortcut { - font-size: 11px; - } - } - - .welcome-links .link-row { - justify-content: center; - } + #welcome-tab { + padding: 24px 16px; + + .welcome-header { + flex-direction: column; + text-align: center; + margin-bottom: 36px; + + .logo { + width: 40px; + max-height: 40px; + } + + .welcome-header-text h1 { + font-size: 18px; + } + } + + .welcome-section { + margin-bottom: 24px; + } + + .action-row { + padding: 8px 10px; + + .action-label { + font-size: 13px; + } + + .action-shortcut { + font-size: 11px; + } } + + .welcome-links .link-row { + justify-content: center; + } + } } // Larger screens - center content better @media (min-width: 600px) { - #welcome-tab { - .welcome-section { - max-width: 480px; - } - - .action-row { - padding: 12px 16px; - } + #welcome-tab { + .welcome-section { + max-width: 480px; } + + .action-row { + padding: 12px 16px; + } + } } // Discord icon .icon.discord { - position: relative; - - &::before { - content: ''; - display: block; - width: 16px; - height: 16px; - background-image: url(../../pages/about/discord.svg); - background-size: contain; - background-repeat: no-repeat; - background-position: center; - } -} \ No newline at end of file + position: relative; + + &::before { + content: ""; + display: block; + width: 16px; + height: 16px; + background-image: url(../../pages/about/discord.svg); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + } +} diff --git a/src/test/sanity.tests.js b/src/test/sanity.tests.js index 6b5938475..61971c8d9 100644 --- a/src/test/sanity.tests.js +++ b/src/test/sanity.tests.js @@ -1,3 +1,4 @@ +import { getLanguageModeRecommendationSearchKeyword } from "../lib/languageModeRecommendations"; import { TestRunner } from "./tester"; export async function runSanityTests(writeOutput) { @@ -63,6 +64,24 @@ export async function runSanityTests(writeOutput) { test.assert(!(value < 5), "Negation should work"); }); + runner.test("Language mode recommendation keywords", (test) => { + test.assertEqual( + getLanguageModeRecommendationSearchKeyword(".gitignore"), + "gitignore", + "Dotfiles without extensions should use the dotfile name", + ); + test.assertEqual( + getLanguageModeRecommendationSearchKeyword("src/main.js"), + "js", + "Normal files should use the file extension", + ); + test.assertEqual( + getLanguageModeRecommendationSearchKeyword("README"), + "", + "Extensionless non-dotfiles should not request plugin recommendations", + ); + }); + // Run all tests return await runner.run(writeOutput); }