diff --git a/src/integrations/WorkspaceTracker.ts b/src/integrations/WorkspaceTracker.ts index 08bf8ba..9ae8199 100644 --- a/src/integrations/WorkspaceTracker.ts +++ b/src/integrations/WorkspaceTracker.ts @@ -28,6 +28,7 @@ class WorkspaceTracker { private registerListeners() { // Listen for file creation + // .bind(this) ensures the callback refers to class instance when using this, not necessary when using arrow function this.disposables.push(vscode.workspace.onDidCreateFiles(this.onFilesCreated.bind(this))) // Listen for file deletion diff --git a/src/providers/ClaudeDevProvider.ts b/src/providers/ClaudeDevProvider.ts index 3555425..cdd5546 100644 --- a/src/providers/ClaudeDevProvider.ts +++ b/src/providers/ClaudeDevProvider.ts @@ -12,6 +12,7 @@ import axios from "axios" import { getTheme } from "../utils/getTheme" import { openFile, openImage } from "../utils/open-file" import WorkspaceTracker from "../integrations/WorkspaceTracker" +import { openMention } from "../utils/context-mentions" /* https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts @@ -423,6 +424,9 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider { case "openFile": openFile(message.text!) break + case "openMention": + openMention(message.text) + break // Add more switch case statements here as more webview message commands // are created within the webview context (i.e. inside media/main.js) } diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index c7bc8e4..ce678aa 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -19,6 +19,7 @@ export interface WebviewMessage { | "requestOllamaModels" | "openImage" | "openFile" + | "openMention" text?: string askResponse?: ClaudeAskResponse apiConfiguration?: ApiConfiguration diff --git a/src/shared/context-mentions.ts b/src/shared/context-mentions.ts new file mode 100644 index 0000000..dfb9e5c --- /dev/null +++ b/src/shared/context-mentions.ts @@ -0,0 +1,9 @@ +/* +Mention regex +- File and folder paths (starting with '/') +- URLs (containing '://') +- The 'problems' keyword +- Word boundary after 'problems' to avoid partial matches +*/ +export const mentionRegex = /@((?:\/|\w+:\/\/)[^\s]+|problems\b)/ +export const mentionRegexGlobal = new RegExp(mentionRegex.source, "g") diff --git a/src/utils/context-mentions.ts b/src/utils/context-mentions.ts new file mode 100644 index 0000000..07e7c2d --- /dev/null +++ b/src/utils/context-mentions.ts @@ -0,0 +1,28 @@ +import * as vscode from "vscode" +import * as path from "path" +import { openFile } from "./open-file" + +export function openMention(mention?: string): void { + if (!mention) { + return + } + + if (mention.startsWith("/")) { + const relPath = mention.slice(1) + const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) + if (!cwd) { + return + } + const absPath = path.resolve(cwd, relPath) + if (mention.endsWith("/")) { + vscode.commands.executeCommand("revealInExplorer", vscode.Uri.file(absPath)) + // vscode.commands.executeCommand("vscode.openFolder", , { forceNewWindow: false }) opens in new window + } else { + openFile(absPath) + } + } else if (mention === "problems") { + vscode.commands.executeCommand("workbench.actions.view.problems") + } else if (mention.startsWith("http")) { + vscode.env.openExternal(vscode.Uri.parse(mention)) + } +} diff --git a/webview-ui/src/components/ChatTextArea.tsx b/webview-ui/src/components/ChatTextArea.tsx index be17507..146e360 100644 --- a/webview-ui/src/components/ChatTextArea.tsx +++ b/webview-ui/src/components/ChatTextArea.tsx @@ -4,15 +4,14 @@ import { useExtensionState } from "../context/ExtensionStateContext" import { getContextMenuOptions, insertMention, - mentionRegex, - mentionRegexGlobal, removeMention, shouldShowContextMenu, ContextMenuOptionType, -} from "../utils/mention-context" +} from "../utils/context-mentions" import { MAX_IMAGES_PER_MESSAGE } from "./ChatView" import ContextMenu from "./ContextMenu" import Thumbnails from "./Thumbnails" +import { mentionRegex, mentionRegexGlobal } from "../../../src/shared/context-mentions" interface ChatTextAreaProps { inputValue: string diff --git a/webview-ui/src/components/ContextMenu.tsx b/webview-ui/src/components/ContextMenu.tsx index 658bc16..f8d4596 100644 --- a/webview-ui/src/components/ContextMenu.tsx +++ b/webview-ui/src/components/ContextMenu.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useMemo, useRef } from "react" -import { ContextMenuOptionType, ContextMenuQueryItem, getContextMenuOptions } from "../utils/mention-context" +import { ContextMenuOptionType, ContextMenuQueryItem, getContextMenuOptions } from "../utils/context-mentions" import { formatFilePathForTruncation } from "./CodeAccordian" interface ContextMenuProps { @@ -109,14 +109,15 @@ const ContextMenu: React.FC = ({ ref={menuRef} style={{ backgroundColor: "var(--vscode-dropdown-background)", - border: "1px solid var(--vscode-dropdown-border)", + border: "1px solid var(--vscode-editorGroup-border)", borderRadius: "3px", + boxShadow: "0 4px 10px rgba(0, 0, 0, 0.25)", zIndex: 1000, display: "flex", flexDirection: "column", - boxShadow: "0 8px 16px rgba(0,0,0,0.24)", maxHeight: "200px", overflowY: "auto", + overflow: "hidden", }}> {/* Can't use virtuoso since it requires fixed height and menu height is dynamic based on # of items */} {filteredOptions.map((option, index) => ( @@ -127,7 +128,7 @@ const ContextMenu: React.FC = ({ padding: "8px 12px", cursor: isOptionSelectable(option) ? "pointer" : "default", color: "var(--vscode-dropdown-foreground)", - borderBottom: "1px solid var(--vscode-dropdown-border)", + borderBottom: "1px solid var(--vscode-editorGroup-border)", display: "flex", alignItems: "center", justifyContent: "space-between", diff --git a/webview-ui/src/components/TaskHeader.tsx b/webview-ui/src/components/TaskHeader.tsx index 2b928e3..fa80912 100644 --- a/webview-ui/src/components/TaskHeader.tsx +++ b/webview-ui/src/components/TaskHeader.tsx @@ -3,9 +3,9 @@ import React, { memo, useEffect, useMemo, useRef, useState } from "react" import { useWindowSize } from "react-use" import { ClaudeMessage } from "../../../src/shared/ExtensionMessage" import { useExtensionState } from "../context/ExtensionStateContext" -import { mentionRegexGlobal } from "../utils/mention-context" import { vscode } from "../utils/vscode" import Thumbnails from "./Thumbnails" +import { mentionRegexGlobal } from "../../../src/shared/context-mentions" interface TaskHeaderProps { task: ClaudeMessage @@ -351,7 +351,9 @@ export const highlightMentions = (text?: string, withShadow = true) => { return ( + className={withShadow ? "mention-context-highlight-with-shadow" : "mention-context-highlight"} + style={{ cursor: "pointer" }} + onClick={() => vscode.postMessage({ type: "openMention", text: part })}> @{part} ) diff --git a/webview-ui/src/utils/mention-context.ts b/webview-ui/src/utils/context-mentions.ts similarity index 92% rename from webview-ui/src/utils/mention-context.ts rename to webview-ui/src/utils/context-mentions.ts index 4b053aa..8286527 100644 --- a/webview-ui/src/utils/mention-context.ts +++ b/webview-ui/src/utils/context-mentions.ts @@ -1,12 +1,4 @@ -/* -Mention regex -- File and folder paths (starting with '/') -- URLs (containing '://') -- The 'problems' keyword -- Word boundary after 'problems' to avoid partial matches -*/ -export const mentionRegex = /@((?:\/|\w+:\/\/)[^\s]+|problems\b)/ -export const mentionRegexGlobal = new RegExp(mentionRegex.source, "g") +import { mentionRegex } from "../../../src/shared/context-mentions" export function insertMention(text: string, position: number, value: string): string { const beforeCursor = text.slice(0, position)