diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e6d5909..a4ec6dbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ - Lots of new UI, misc bug fixes, and performance improvements. +- VS Code's default Ctrl+L is now Ctrl+M in Void (on Mac Cmd+L becomes Cmd+M). + - Switched from the MIT License to the Apache 2.0 License. Apache's attribution clause provides a small amount of protection to our source initiative. A huge shoutout to our many contributors. If you'd like to help build Void, diff --git a/build/darwin/create-universal-app.js b/build/darwin/create-universal-app.js index a3daf187..e6a355d5 100644 --- a/build/darwin/create-universal-app.js +++ b/build/darwin/create-universal-app.js @@ -4,6 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); +// Void explanation - product-build-darwin-universal.yml runs this (create-universal-app.ts), then sign.ts const path = require("path"); const fs = require("fs"); const minimatch = require("minimatch"); diff --git a/build/darwin/sign.js b/build/darwin/sign.js index feb5834f..90c2e825 100644 --- a/build/darwin/sign.js +++ b/build/darwin/sign.js @@ -78,24 +78,24 @@ async function main(buildDir) { // universal will get its copy from the x64 build. if (arch !== 'universal') { await (0, cross_spawn_promise_1.spawn)('plutil', [ - '-insert', + '-replace', // Void changed this to replace 'NSAppleEventsUsageDescription', '-string', - 'An application in Visual Studio Code wants to use AppleScript.', + 'An application in Void wants to use AppleScript.', `${infoPlistPath}` ]); await (0, cross_spawn_promise_1.spawn)('plutil', [ '-replace', 'NSMicrophoneUsageDescription', '-string', - 'An application in Visual Studio Code wants to use the Microphone.', + 'An application in Void wants to use the Microphone.', `${infoPlistPath}` ]); await (0, cross_spawn_promise_1.spawn)('plutil', [ '-replace', 'NSCameraUsageDescription', '-string', - 'An application in Visual Studio Code wants to use the Camera.', + 'An application in Void wants to use the Camera.', `${infoPlistPath}` ]); } diff --git a/build/darwin/sign.ts b/build/darwin/sign.ts index 9e605801..a41ca30d 100644 --- a/build/darwin/sign.ts +++ b/build/darwin/sign.ts @@ -89,7 +89,7 @@ async function main(buildDir?: string): Promise { // universal will get its copy from the x64 build. if (arch !== 'universal') { await spawn('plutil', [ - '-insert', + '-replace', // Void changed this to replace 'NSAppleEventsUsageDescription', '-string', 'An application in Void wants to use AppleScript.', diff --git a/extensions/void/LangaugeServerTest/createJsProgramGraph.ts b/extensions/void/LangaugeServerTest/createJsProgramGraph.ts deleted file mode 100644 index 4fb9bcaf..00000000 --- a/extensions/void/LangaugeServerTest/createJsProgramGraph.ts +++ /dev/null @@ -1,333 +0,0 @@ -import * as vscode from 'vscode'; -import Parser from 'tree-sitter'; -import JavaScript from 'tree-sitter-javascript'; - -interface Definition { - file: string; - node: Parser.SyntaxNode; -} - -interface DefnUse { - parent: Parser.SyntaxNode; - file: string; -} - -interface ImportInfo { - source: string; - imported: string; -} - -class ProjectAnalyzer { - private parser: Parser; - private graph: Map>; - private visited: Set; - private parsedFiles: Map; - private imports: Map>; - private definitions: Map; - private fileStack: Set; - - constructor() { - this.parser = new Parser(); - this.parser.setLanguage(JavaScript); - this.graph = new Map(); - this.visited = new Set(); - this.parsedFiles = new Map(); - this.imports = new Map(); - this.definitions = new Map(); - this.fileStack = new Set(); - } - - async parseFile(filePath: string): Promise { - if (this.parsedFiles.has(filePath)) { - return this.parsedFiles.get(filePath)!; - } - - if (this.fileStack.has(filePath)) { - return null; // Circular import - } - - this.fileStack.add(filePath); - - try { - const uri = vscode.Uri.file(filePath); - const document = await vscode.workspace.openTextDocument(uri); - const code = document.getText(); - const tree = this.parser.parse(code); - - this.parsedFiles.set(filePath, tree); - this.collectImports(filePath, tree); - this.collectDefinitions(filePath, tree); - - return tree; - } catch (error) { - console.error(`Error parsing ${filePath}:`, error); - return null; - } finally { - this.fileStack.delete(filePath); - } - } - - private collectImports(filePath: string, tree: Parser.Tree): void { - const fileImports = new Map(); - - const visit = (node: Parser.SyntaxNode): void => { - if (node.type === 'import_declaration') { - const source = node.childForFieldName('source')?.text.slice(1, -1) ?? ''; - const specifiers = node.childForFieldName('specifiers'); - - specifiers?.children.forEach(spec => { - if (spec.type === 'import_specifier') { - const local = spec.childForFieldName('local')?.text ?? ''; - const imported = spec.childForFieldName('imported')?.text ?? ''; - fileImports.set(local, { source, imported }); - } - }); - } - node.children.forEach(visit); - }; - - visit(tree.rootNode); - this.imports.set(filePath, fileImports); - } - - private collectDefinitions(filePath: string, tree: Parser.Tree): void { - const visit = (node: Parser.SyntaxNode): void => { - if (node.type === 'function_declaration') { - const name = node.childForFieldName('name')?.text ?? ''; - this.definitions.set(name, { file: filePath, node }); - } - else if (node.type === 'variable_declarator') { - const name = node.childForFieldName('name')?.text; - const value = node.childForFieldName('value'); - if (name && (value?.type === 'arrow_function' || value?.type === 'function')) { - this.definitions.set(name, { file: filePath, node: value }); - } - } - node.children.forEach(visit); - }; - - visit(tree.rootNode); - } - - private async getTypeFromPosition(uri: vscode.Uri, position: vscode.Position): Promise { - const hover = await vscode.commands.executeCommand( - 'vscode.executeHoverProvider', - uri, - position - ); - - if (hover?.[0]?.contents.length) { - for (const content of hover[0].contents) { - let hoverText = typeof content === 'string' ? - content : - ('value' in content ? content.value : ''); - - // Remove typescript backticks if present - hoverText = hoverText.replace(/```typescript\s*/, '').replace(/```\s*$/, ''); - console.log('Processing hover text:', hoverText); - - // Extract the type information - look for the type after the colon - const typeMatches = [ - /:\s*([\w<>]+)(?:\[\])?/, // matches "foo: Type" or "foo: Type[]" - /var\s+\w+:\s*([\w<>]+)/, // matches "var foo: Type" - /\(type\)\s+[\w<>]+:\s*([\w<>]+)/, // matches "(type) foo: Type" - /\(method\)\s*([\w<>]+)\./ // matches "(method) Type.method" - ]; - - for (const pattern of typeMatches) { - const match = pattern.exec(hoverText); - if (match) { - let type = match[1]; - // Handle array types - if (hoverText.includes('[]')) { - return 'Array'; - } - // Extract base type from generics - if (type.includes('<')) { - type = type.split('<')[0]; - } - return type; - } - } - } - } - return null; - } - - private async getCallsInDefn(defnNode: Parser.SyntaxNode, currentFile: string): Promise> { - const calls = new Set(); - const fileImports = this.imports.get(currentFile) ?? new Map(); - const uri = vscode.Uri.file(currentFile); - - const visit = async (node: Parser.SyntaxNode): Promise => { - if (node.type === 'call_expression') { - const callee = node.childForFieldName('function'); - if (callee?.type === 'identifier') { - const name = callee.text; - const importInfo = fileImports.get(name); - if (importInfo) { - calls.add(`${importInfo.source}:${importInfo.imported}`); - } else { - calls.add(name); - } - } - else if (callee?.type === 'member_expression') { - const method = callee.childForFieldName('property')?.text; - const object = callee.childForFieldName('object'); - - if (method && object) { - const position = new vscode.Position( - object.startPosition.row, - object.startPosition.column - ); - - const type = await this.getTypeFromPosition(uri, position); - if (type) { - calls.add(`${type}.${method}`); - } else { - calls.add(`method:${method}`); - } - } - } - } - - for (const child of node.children) { - await visit(child); - } - }; - - await visit(defnNode); - return calls; - } - - private gotoDefn(name: string): Definition | null { - if (name.includes(':')) { - const [file, funcName] = name.split(':'); - const def = this.definitions.get(funcName); - return def ?? null; - } - - return this.definitions.get(name) ?? null; - } - - private getUses(defnNode: Parser.SyntaxNode, currentFile: string): DefnUse[] { - const uses: DefnUse[] = []; - - let fnName: string | undefined; - if (defnNode.type === 'function_declaration') { - fnName = defnNode.childForFieldName('name')?.text; - } else if (defnNode.type === 'arrow_function' || defnNode.type === 'function') { - const parent = defnNode.parent; - if (parent?.type === 'variable_declarator') { - fnName = parent.childForFieldName('name')?.text; - } - } - - if (!fnName) return uses; - - for (const [file, tree] of this.parsedFiles) { - const visit = (node: Parser.SyntaxNode): void => { - if (node.type === 'call_expression') { - const callee = node.childForFieldName('function'); - if (callee?.type === 'identifier' && callee.text === fnName) { - let current: Parser.SyntaxNode | null = node; - while (current) { - if (current.type === 'function_declaration' || - current.type === 'arrow_function' || - current.type === 'function') { - uses.push({ parent: current, file }); - break; - } - current = current.parent; - } - } - } - node.children.forEach(visit); - }; - - visit(tree.rootNode); - } - - return uses; - } - - private async visitAllNodesInGraphFromDefinition(defn: Parser.SyntaxNode, currentFile: string): Promise { - let defnName: string | undefined; - if (defn.type === 'function_declaration') { - defnName = defn.childForFieldName('name')?.text; - } else if (defn.type === 'arrow_function' || defn.type === 'function') { - const parent = defn.parent; - if (parent?.type === 'variable_declarator') { - defnName = parent.childForFieldName('name')?.text; - } - } - - if (!defnName) return; - - const fullName = `${currentFile}:${defnName}`; - if (this.visited.has(fullName)) return; - - const calls = await this.getCallsInDefn(defn, currentFile); - this.graph.set(fullName, calls); - this.visited.add(fullName); - - const callDefns = Array.from(calls).map(call => this.gotoDefn(call)); - for (const callDefn of callDefns) { - if (callDefn) { - await this.visitAllNodesInGraphFromDefinition(callDefn.node, callDefn.file); - } - } - - const defnUses = this.getUses(defn, currentFile); - for (const defnUse of defnUses) { - await this.visitAllNodesInGraphFromDefinition(defnUse.parent, defnUse.file); - } - } - - async analyze(entryFile: string): Promise>> { - const tree = await this.parseFile(entryFile); - if (!tree) return new Map(); - - const visit = async (node: Parser.SyntaxNode): Promise => { - if (node.type === 'function_declaration') { - await this.visitAllNodesInGraphFromDefinition(node, entryFile); - } - else if (node.type === 'variable_declarator') { - const value = node.childForFieldName('value'); - if (value?.type === 'arrow_function' || value?.type === 'function') { - await this.visitAllNodesInGraphFromDefinition(value, entryFile); - } - } - for (const child of node.children) { - await visit(child); - } - }; - - await visit(tree.rootNode); - return this.graph; - } -} - -export async function runTreeSitter(filePath?: string): Promise> | null> { - const editor = vscode.window.activeTextEditor; - if (!editor && !filePath) { - vscode.window.showWarningMessage('No active editor found'); - return null; - } - - try { - const targetPath = filePath ?? editor!.document.uri.fsPath; - const analyzer = new ProjectAnalyzer(); - const graph = await analyzer.analyze(targetPath); - - for (const [defn, calls] of graph) { - console.log(`${defn} calls: ${[...calls].join(', ')}`); - } - - return graph; - } catch (error) { - console.error('Error analyzing file:', error); - vscode.window.showErrorMessage('Error analyzing file'); - return null; - } -} \ No newline at end of file diff --git a/extensions/void/LangaugeServerTest/findFunctions.ts b/extensions/void/LangaugeServerTest/findFunctions.ts deleted file mode 100644 index 570b4369..00000000 --- a/extensions/void/LangaugeServerTest/findFunctions.ts +++ /dev/null @@ -1,62 +0,0 @@ -import * as vscode from 'vscode'; - -const legend = new vscode.SemanticTokensLegend([], []); - -export async function findFunctions() { - - const editor = vscode.window.activeTextEditor; - if (!editor) return; - const document = editor.document; - - const tokens = await vscode.commands.executeCommand( - 'vscode.provideDocumentSemanticTokens', - document.uri - ); - - if (!tokens) { - console.error('No tokens found'); - return []; - } - - const allTokens = decodeTokens(tokens, document); - - - return allTokens; -} - -function decodeTokens(tokens: vscode.SemanticTokens, document: vscode.TextDocument) { - const data = tokens.data; - const decodedTokens = []; - let line = 0; - let character = 0; - - for (let i = 0; i < data.length; i += 5) { - const deltaLine = data[i]; - const deltaStartChar = data[i + 1]; - const length = data[i + 2]; - const tokenTypeIdx = data[i + 3]; - const tokenModifierIdx = data[i + 4]; - - line += deltaLine; - character = deltaLine === 0 ? character + deltaStartChar : deltaStartChar; - - const type = legend.tokenTypes[tokenTypeIdx] || `(${tokenTypeIdx})`; - const modifier = legend.tokenModifiers[tokenModifierIdx] || `(${tokenModifierIdx})`; - - const tokenRange = new vscode.Range(line, character, line, character + length); - const tokenText = document.getText(tokenRange); - - decodedTokens.push({ - line, - startCharacter: character, - length, - type, - modifier, - text: tokenText, - }); - - console.log(`Token: '${tokenText}' | Type: ${type} | Modifier: ${modifier} | Line: ${line}, Character: ${character}`); - } - - return decodedTokens; -} diff --git a/src/vs/platform/extensions/common/extensionValidator.ts b/src/vs/platform/extensions/common/extensionValidator.ts index 94f3bcf8..e9736b22 100644 --- a/src/vs/platform/extensions/common/extensionValidator.ts +++ b/src/vs/platform/extensions/common/extensionValidator.ts @@ -404,8 +404,8 @@ function isVersionValid(currentVersion: string, date: ProductDate, requestedVers if (!isValidVersion(currentVersion, date, desiredVersion)) { // Void - ignore not compatible - // notices.push(nls.localize('versionMismatch', "Extension is not compatible with Code {0}. Extension requires: {1}.", currentVersion, requestedVersion)); - // return false; + notices.push(nls.localize('versionMismatch', "Extension is not compatible with Code {0}. Extension requires: {1}.", currentVersion, requestedVersion)); + return false; } return true; diff --git a/src/vs/platform/keybinding/common/keybindingsRegistry.ts b/src/vs/platform/keybinding/common/keybindingsRegistry.ts index eeabcc55..e83230ae 100644 --- a/src/vs/platform/keybinding/common/keybindingsRegistry.ts +++ b/src/vs/platform/keybinding/common/keybindingsRegistry.ts @@ -64,7 +64,8 @@ export const enum KeybindingWeight { EditorContrib = 100, WorkbenchContrib = 200, BuiltinExtension = 300, - ExternalExtension = 400 + ExternalExtension = 400, + VoidExtension = 605, // Void - must trump any external extension } export interface ICommandAndKeybindingRule extends IKeybindingRule { diff --git a/src/vs/platform/telemetry/common/telemetryService.ts b/src/vs/platform/telemetry/common/telemetryService.ts index c3e05eb2..eb04c7a1 100644 --- a/src/vs/platform/telemetry/common/telemetryService.ts +++ b/src/vs/platform/telemetry/common/telemetryService.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { DisposableStore } from '../../../base/common/lifecycle.js'; +// import { mixin } from '../../../base/common/objects.js'; import { isWeb } from '../../../base/common/platform.js'; import { escapeRegExpCharacters } from '../../../base/common/strings.js'; import { localize } from '../../../nls.js'; @@ -15,6 +16,7 @@ import { Registry } from '../../registry/common/platform.js'; import { ClassifiedEvent, IGDPRProperty, OmitMetadata, StrictPropertyCheck } from './gdprTypings.js'; import { ITelemetryData, ITelemetryService, TelemetryConfiguration, TelemetryLevel, TELEMETRY_CRASH_REPORTER_SETTING_ID, TELEMETRY_OLD_SETTING_ID, TELEMETRY_SECTION_ID, TELEMETRY_SETTING_ID, ICommonProperties } from './telemetry.js'; import { getTelemetryLevel, ITelemetryAppender } from './telemetryUtils.js'; +// import { cleanData } from './telemetryUtils.js'; export interface ITelemetryServiceConfig { appenders: ITelemetryAppender[]; diff --git a/src/vs/platform/void/common/llmMessageService.ts b/src/vs/platform/void/common/llmMessageService.ts index 0bd8bf13..caaeb0c8 100644 --- a/src/vs/platform/void/common/llmMessageService.ts +++ b/src/vs/platform/void/common/llmMessageService.ts @@ -65,7 +65,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService this._onRequestIdDone(e.requestId) })) this._register((this.channel.listen('onError_llm') satisfies Event)(e => { - console.log('Error in LLMMessageService:', JSON.stringify(e)) + console.error('Error in LLMMessageService:', JSON.stringify(e)) this.onErrorHooks_llm[e.requestId]?.(e) this._onRequestIdDone(e.requestId) })) diff --git a/src/vs/platform/void/common/metricsService.ts b/src/vs/platform/void/common/metricsService.ts index 7002af45..a3aeb6a8 100644 --- a/src/vs/platform/void/common/metricsService.ts +++ b/src/vs/platform/void/common/metricsService.ts @@ -7,10 +7,15 @@ import { createDecorator } from '../../instantiation/common/instantiation.js'; import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js'; import { IMainProcessService } from '../../ipc/common/mainProcessService.js'; import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js'; +import { Action2, registerAction2 } from '../../actions/common/actions.js'; +import { localize2 } from '../../../nls.js'; +import { ServicesAccessor } from '../../../editor/browser/editorExtensions.js'; +import { INotificationService } from '../../notification/common/notification.js'; export interface IMetricsService { readonly _serviceBrand: undefined; capture(event: string, params: Record): void; + getDebuggingProperties(): Promise; } export const IMetricsService = createDecorator('metricsService'); @@ -34,7 +39,30 @@ export class MetricsService implements IMetricsService { this.metricsService.capture(...params); } + // anything transmitted over a channel must be async even if it looks like it doesn't have to be + async getDebuggingProperties(): Promise { + return this.metricsService.getDebuggingProperties() + } } registerSingleton(IMetricsService, MetricsService, InstantiationType.Eager); + +// debugging action +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'voidDebugInfo', + f1: true, + title: localize2('voidMetricsDebug', 'Void: Log Debug Info'), + }); + } + async run(accessor: ServicesAccessor): Promise { + const metricsService = accessor.get(IMetricsService) + const notifService = accessor.get(INotificationService) + + const debugProperties = await metricsService.getDebuggingProperties() + console.log('Metrics:', debugProperties) + notifService.info(`Void Debug info:\n${JSON.stringify(debugProperties, null, 2)}`) + } +}) diff --git a/src/vs/platform/void/electron-main/metricsMainService.ts b/src/vs/platform/void/electron-main/metricsMainService.ts index e1811e4c..fdfb1d16 100644 --- a/src/vs/platform/void/electron-main/metricsMainService.ts +++ b/src/vs/platform/void/electron-main/metricsMainService.ts @@ -5,50 +5,96 @@ import { Disposable } from '../../../base/common/lifecycle.js'; import { isLinux, isMacintosh, isWindows } from '../../../base/common/platform.js'; +import { generateUuid } from '../../../base/common/uuid.js'; +import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js'; + import { IProductService } from '../../product/common/productService.js'; -import { ITelemetryService } from '../../telemetry/common/telemetry.js'; +import { IStorageMainService } from '../../storage/electron-main/storageMainService.js'; import { IMetricsService } from '../common/metricsService.js'; import { PostHog } from 'posthog-node' -// posthog-js (old): -// posthog.init('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', { api_host: 'https://us.i.posthog.com', }) +const os = isWindows ? 'windows' : isMacintosh ? 'mac' : isLinux ? 'linux' : null -// const buildEnv = 'development'; -// const buildNumber = '1.0.0'; -// const isMac = process.platform === 'darwin'; +const VOID_MACHINE_STORAGE_KEY = 'void.machineId' export class MetricsMainService extends Disposable implements IMetricsService { _serviceBrand: undefined; - readonly _distinctId: string - readonly client: PostHog + private readonly client: PostHog + + private readonly _initProperties: object + + + // TODO we should eventually identify people based on email + private get machineId() { + const currVal = this._storageService.applicationStorage.get(VOID_MACHINE_STORAGE_KEY) + if (currVal !== undefined) return currVal + const newVal = generateUuid() + this._storageService.applicationStorage.set(VOID_MACHINE_STORAGE_KEY, newVal) + return newVal + } + constructor( - @ITelemetryService private readonly _telemetryService: ITelemetryService, - @IProductService private readonly _productService: IProductService + @IProductService private readonly _productService: IProductService, + @IStorageMainService private readonly _storageService: IStorageMainService, + @IEnvironmentMainService private readonly _envMainService: IEnvironmentMainService, ) { super() - this.client = new PostHog('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', { host: 'https://us.i.posthog.com', }) + this.client = new PostHog('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', { + host: 'https://us.i.posthog.com', + }) - const { devDeviceId, firstSessionDate, machineId } = this._telemetryService + // we'd like to use devDeviceId on telemetryService, but that gets sanitized by the time it gets here as 'someValue.devDeviceId' - this._distinctId = devDeviceId + const { commit, version, quality } = this._productService - const { commit, version } = this._productService - const os = isWindows ? 'windows' : isMacintosh ? 'mac' : isLinux ? 'linux' : null + const isDevMode = !this._envMainService.isBuilt // found in abstractUpdateService.ts - this.client.identify({ distinctId: this._distinctId, properties: { firstSessionDate, machineId, commit, version, os } }) - console.log('Void posthog metrics info:', JSON.stringify({ devDeviceId, firstSessionDate, machineId })) + // custom properties we identify + this._initProperties = { + commit, + version, + os, + quality, + distinctId: this.machineId, + isDevMode, + ...this._getOSInfo(), + } + + const identifyMessage = { + distinctId: this.machineId, + properties: this._initProperties, + } + this.client.identify(identifyMessage) + + console.log('Void posthog metrics info:', JSON.stringify(identifyMessage, null, 2)) + + } + + _getOSInfo() { + try { + const { platform, arch } = process // see platform.ts + return { platform, arch } + } + catch (e) { + return { osInfo: { platform: '??', arch: '??' } } + } } capture: IMetricsService['capture'] = (event, params) => { - const capture = { distinctId: this._distinctId, event, properties: params } as const + const capture = { distinctId: this.machineId, event, properties: params } as const // console.log('full capture:', capture) this.client.capture(capture) } + + + async getDebuggingProperties() { + return this._initProperties + } } diff --git a/src/vs/workbench/browser/media/code-icon.svg b/src/vs/workbench/browser/media/code-icon.svg deleted file mode 100644 index cc61f81e..00000000 --- a/src/vs/workbench/browser/media/code-icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/browser/media/void-icon-sm.png b/src/vs/workbench/browser/media/void-icon-sm.png new file mode 100644 index 00000000..45816670 Binary files /dev/null and b/src/vs/workbench/browser/media/void-icon-sm.png differ diff --git a/src/vs/workbench/browser/parts/banner/media/bannerpart.css b/src/vs/workbench/browser/parts/banner/media/bannerpart.css index a0de81f2..90e26053 100644 --- a/src/vs/workbench/browser/parts/banner/media/bannerpart.css +++ b/src/vs/workbench/browser/parts/banner/media/bannerpart.css @@ -30,7 +30,7 @@ background-repeat: no-repeat; background-position: center center; background-size: 16px; - background-image: url('../../../../browser/media/code-icon.svg'); + background-image: url('../../../../browser/media/void-icon-sm.png'); width: 16px; padding: 0; margin: 0 6px 0 10px; diff --git a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css index ccc39382..221634bb 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css +++ b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css @@ -253,7 +253,7 @@ } .monaco-workbench .part.titlebar > .titlebar-container > .titlebar-left > .window-appicon:not(.codicon) { - background-image: url('../../../media/code-icon.svg'); + background-image: url('../../../media/void-icon-sm.png'); background-repeat: no-repeat; background-position: center center; background-size: 16px; @@ -275,7 +275,7 @@ height: 8px; z-index: 1; /* on top of home indicator */ - background-image: url('../../../media/code-icon.svg'); + background-image: url('../../../media/void-icon-sm.png'); background-repeat: no-repeat; background-position: center center; background-size: 8px; diff --git a/src/vs/workbench/contrib/update/browser/media/releasenoteseditor.css b/src/vs/workbench/contrib/update/browser/media/releasenoteseditor.css index 4210055b..b79e3905 100644 --- a/src/vs/workbench/contrib/update/browser/media/releasenoteseditor.css +++ b/src/vs/workbench/contrib/update/browser/media/releasenoteseditor.css @@ -5,5 +5,5 @@ .file-icons-enabled .show-file-icons .webview-vs_code_release_notes-name-file-icon.file-icon::before { content: ' '; - background-image: url('../../../../browser/media/code-icon.svg'); + background-image: url('../../../../browser/media/void-icon-sm.png'); } diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 0faf4725..7fede377 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -74,9 +74,9 @@ export type ThreadsState = { export type ThreadStreamState = { [threadId: string]: undefined | { - streamingToken?: string; error?: { message: string, fullError: Error | null }; messageSoFar?: string; + streamingToken?: string; } } @@ -177,6 +177,13 @@ class ChatThreadService extends Disposable implements IChatThreadService { // ---------- streaming ---------- + finishStreaming = (threadId: string, content: string, error?: { message: string, fullError: Error | null }) => { + // add assistant's message to chat history, and clear selection + const assistantHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content || null } + this._addMessageToThread(threadId, assistantHistoryElt) + this._setStreamState(threadId, { messageSoFar: undefined, streamingToken: undefined, error }) + } + async addUserMessageAndStreamResponse(userMessage: string) { const threadId = this.getCurrentThread().id @@ -192,12 +199,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { const userHistoryElt: ChatMessage = { role: 'user', content: chat_prompt(instructions, selections), displayContent: instructions, selections: selections } this._addMessageToThread(threadId, userHistoryElt) - const onDone = (content: string, error?: { message: string, fullError: Error | null }) => { - // add assistant's message to chat history, and clear selection - const assistantHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content || null } - this._addMessageToThread(threadId, assistantHistoryElt) - this._setStreamState(threadId, { messageSoFar: undefined, streamingToken: undefined, error }) - } this._setStreamState(threadId, { error: undefined }) @@ -211,11 +212,10 @@ class ChatThreadService extends Disposable implements IChatThreadService { this._setStreamState(threadId, { messageSoFar: fullText }) }, onFinalMessage: ({ fullText: content }) => { - onDone(content) + this.finishStreaming(threadId, content) }, onError: (error) => { - console.log('Void Chat Error:', error) - onDone(this.streamState[threadId]?.messageSoFar ?? '', error) + this.finishStreaming(threadId, this.streamState[threadId]?.messageSoFar ?? '', error) }, useProviderFor: 'Ctrl+L', @@ -227,8 +227,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { cancelStreaming(threadId: string) { const llmCancelToken = this.streamState[threadId]?.streamingToken - if (llmCancelToken) this._llmMessageService.abort(llmCancelToken) - this._setStreamState(threadId, { streamingToken: undefined }) + if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken) + this.finishStreaming(threadId, this.streamState[threadId]?.messageSoFar ?? '') } dismissStreamError(threadId: string): void { diff --git a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts index d6d04ade..76bcbd8c 100644 --- a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts +++ b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts @@ -39,6 +39,8 @@ import { INotificationService, Severity } from '../../../../platform/notificatio import { isMacintosh } from '../../../../base/common/platform.js'; import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; import { Emitter } from '../../../../base/common/event.js'; +import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; const configOfBG = (color: Color) => { return { dark: color, light: color, hcDark: color, hcLight: color, } @@ -249,6 +251,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { @IConsistentEditorItemService private readonly _consistentEditorItemService: IConsistentEditorItemService, @IMetricsService private readonly _metricsService: IMetricsService, @INotificationService private readonly _notificationService: INotificationService, + @ICommandService private readonly _commandService: ICommandService, ) { super(); @@ -596,7 +599,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { )); const viewZone: IViewZone = { - afterLineNumber: type === 'edit' ? diff.endLine : diff.startLine - 1, + afterLineNumber: diff.startLine - 1, heightInLines, minWidthInPx, domNode, @@ -623,6 +626,26 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { const consistentWidgetId = this._consistentItemService.addConsistentItemToURI({ uri, fn: (editor) => { + let startLine: number + let offsetLines: number + if (diff.type === 'insertion' || diff.type === 'edit') { + startLine = diff.startLine // green start + offsetLines = 0 + } + else if (diff.type === 'deletion') { + // if diff.startLine is out of bounds + if (diff.startLine === 1) { + const numRedLines = diff.originalEndLine - diff.originalStartLine + 1 + startLine = diff.startLine + offsetLines = -numRedLines + } + else { + startLine = diff.startLine - 1 + offsetLines = 1 + } + } + else { throw 1 } + const buttonsWidget = new AcceptRejectWidget({ editor, onAccept: () => { @@ -634,13 +657,8 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { this._metricsService.capture('Reject Diff', {}) }, diffid: diffid.toString(), - startLine: diff.startLine, - offsetLines: ( - diff.type === 'insertion' ? 0 - : diff.type === 'deletion' ? -(diff.originalEndLine - diff.originalStartLine + 1) - : diff.type === 'edit' ? (diff.endLine - diff.startLine + 1) - : 0 // not allowed - ) + startLine, + offsetLines }) return () => { buttonsWidget.dispose() } } @@ -1357,11 +1375,20 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { onDone(false) }, onError: (e) => { - console.error('Error rewriting file with diff', e); const details = errorDetails(e.fullError) this._notificationService.notify({ severity: Severity.Warning, message: `Void Error: ${e.message}`, + actions: { + secondary: [{ + id: 'void.onerror.opensettings', + enabled: true, + label: 'Open Void settings', + tooltip: '', + class: undefined, + run: () => { this._commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) } + }] + }, source: details ? `(Hold ${isMacintosh ? 'Option' : 'Alt'} to hover) - ${details}` : undefined }) onDone(true) @@ -1806,7 +1833,7 @@ class AcceptAllRejectAllWidget extends Widget implements IOverlayWidget { ]); // Style the container - buttons.style.zIndex = '1'; + buttons.style.zIndex = '2'; buttons.style.padding = '4px'; buttons.style.display = 'flex'; buttons.style.gap = '4px'; diff --git a/src/vs/workbench/contrib/void/browser/quickEditActions.ts b/src/vs/workbench/contrib/void/browser/quickEditActions.ts index 125e983c..1498c8f6 100644 --- a/src/vs/workbench/contrib/void/browser/quickEditActions.ts +++ b/src/vs/workbench/contrib/void/browser/quickEditActions.ts @@ -12,6 +12,7 @@ import { ICodeEditorService } from '../../../../editor/browser/services/codeEdit import { IInlineDiffsService } from './inlineDiffsService.js'; import { roundRangeToLines } from './sidebarActions.js'; import { VOID_CTRL_K_ACTION_ID } from './actionIDs.js'; +import { localize2 } from '../../../../nls.js'; export type QuickEditPropsType = { @@ -37,10 +38,11 @@ registerAction2(class extends Action2 { ) { super({ id: VOID_CTRL_K_ACTION_ID, - title: 'Void: Quick Edit', + f1: true, + title: localize2('voidQuickEditAction', 'Void: Quick Edit'), keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyK, - weight: KeybindingWeight.BuiltinExtension, + weight: KeybindingWeight.VoidExtension, } }); } diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx index fca06caa..43abd5b1 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx @@ -9,23 +9,21 @@ import { VoidCodeEditor, VoidCodeEditorProps } from '../util/inputs.js'; export const BlockCode = ({ buttonsOnHover, ...codeEditorProps }: { buttonsOnHover?: React.ReactNode } & VoidCodeEditorProps) => { - const isSingleLine = !codeEditorProps.initValue.includes('\n') return ( <> -
- +
{buttonsOnHover === null ? null : ( -
-
+
+
{buttonsOnHover}
)} -
) diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/Sidebar.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/Sidebar.tsx index fe757a96..839a6679 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/Sidebar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/Sidebar.tsx @@ -28,7 +28,7 @@ export const Sidebar = ({ className }: { className: string }) => {
) => { @@ -259,9 +260,9 @@ const getBasename = (pathStr: string) => { } export const SelectedFiles = ( - { type, selections, setStaging }: - | { type: 'past', selections: CodeSelection[] | null; setStaging?: undefined } - | { type: 'staging', selections: CodeStagingSelection[] | null; setStaging: ((files: CodeStagingSelection[]) => void) } + { type, selections, setSelections, showProspectiveSelections }: + | { type: 'past', selections: CodeSelection[]; setSelections?: undefined, showProspectiveSelections?: undefined } + | { type: 'staging', selections: CodeStagingSelection[]; setSelections: ((newSelections: CodeStagingSelection[]) => void), showProspectiveSelections?: boolean } ) => { // index -> isOpened @@ -273,85 +274,123 @@ export const SelectedFiles = ( const accessor = useAccessor() const commandService = accessor.get('ICommandService') + // state for tracking prospective files + const { currentUri } = useUriState() + const [recentUris, setRecentUris] = useState([]) + const maxRecentUris = 10 + const maxProspectiveFiles = 3 + useEffect(() => { // handle recent files + if (!currentUri) return + setRecentUris(prev => { + const withoutCurrent = prev.filter(uri => uri.fsPath !== currentUri.fsPath) // remove duplicates + const withCurrent = [currentUri, ...withoutCurrent] + return withCurrent.slice(0, maxRecentUris) + }) + }, [currentUri]) + let prospectiveSelections: CodeStagingSelection[] = [] + if (type === 'staging' && showProspectiveSelections) { // handle prospective files + // add a prospective file if type === 'staging' and if the user is in a file, and if the file is not selected yet + prospectiveSelections = recentUris + .filter(uri => !selections.find(s => s.range === null && s.fileURI.fsPath === uri.fsPath)) + .slice(0, maxProspectiveFiles) + .map(uri => ({ + type: 'File', + fileURI: uri, + selectionStr: null, + range: null, + })) + } + + const allSelections = [...selections, ...prospectiveSelections] + + if (allSelections.length === 0) { + return null + } + return ( - !!selections && selections.length !== 0 && ( -
- {selections.map((selection, i) => { +
- const isThisSelectionOpened = !!(selection.selectionStr && selectionIsOpened[i]) - const isThisSelectionAFile = selection.selectionStr === null + {allSelections.map((selection, i) => { - return
selections.length - 1 + + const thisKey = `${isThisSelectionProspective}-${i}-${selections.length}` + + const selectionHTML = (
+ {/* selection summary */} +
- {/* selection summary */} -
-
{ - // open the file if it is a file - if (isThisSelectionAFile) { - commandService.executeCommand('vscode.open', selection.fileURI, { - preview: true, - // preserveFocus: false, - }); - } else { - // open the selection if it is a text-selection - setSelectionIsOpened(s => { - const newS = [...s] - newS[i] = !newS[i] - return newS - }); - } - }} - > - - {/* file name */} - {getBasename(selection.fileURI.fsPath)} - {/* selection range */} - {!isThisSelectionAFile ? ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})` : ''} - + onClick={() => { + if (isThisSelectionProspective) { // add prospective selection to selections + if (type !== 'staging') return; // (never) + setSelections([...selections, selection as CodeStagingSelection]) - {/* X button */} - {type === 'staging' && - { - e.stopPropagation(); // don't open/close selection - if (type !== 'staging') return; - setStaging([...selections.slice(0, i), ...selections.slice(i + 1)]) - setSelectionIsOpened(o => [...o.slice(0, i), ...o.slice(i + 1)]) - }} - > - - } + } else if (isThisSelectionAFile) { // open files + commandService.executeCommand('vscode.open', selection.fileURI, { + preview: true, + // preserveFocus: false, + }); + } else { // show text + setSelectionIsOpened(s => { + const newS = [...s] + newS[i] = !newS[i] + return newS + }); + } + }} + > + + {/* file name */} + {getBasename(selection.fileURI.fsPath)} + {/* selection range */} + {!isThisSelectionAFile ? ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})` : ''} + + + {/* X button */} + {type === 'staging' && !isThisSelectionProspective && + { + e.stopPropagation(); // don't open/close selection + if (type !== 'staging') return; + setSelections([...selections.slice(0, i), ...selections.slice(i + 1)]) + setSelectionIsOpened(o => [...o.slice(0, i), ...o.slice(i + 1)]) + }} + > + + } -
+
- {/* clear all selections button */} - {type !== 'staging' || selections.length === 0 || i !== selections.length - 1 - ? null - :
-
setIsClearHovered(true)} - onMouseLeave={() => setIsClearHovered(false)} - > - +
setIsClearHovered(true)} + onMouseLeave={() => setIsClearHovered(false)} + > + { setStaging([]) }} - /> -
+ onClick={() => { setSelections([]) }} + />
- } -
- {/* selection text */} - {isThisSelectionOpened && -
{ - e.stopPropagation(); // don't focus input box - }} - > -
}
+ {/* selection text */} + {isThisSelectionOpened && +
{ + e.stopPropagation(); // don't focus input box + }} + > + +
+ } +
) - })} + return + {selections.length > 0 && i === selections.length && +
// divider between `selections` and `prospectiveSelections` + } + {selectionHTML} +
+ + })} -
- ) +
+ ) } -const ChatBubble = ({ chatMessage, isLoading }: { - chatMessage: ChatMessage, - isLoading?: boolean, -}) => { +const ChatBubble_ = ({ isEditMode, isLoading, children, role }: { role: ChatMessage['role'], children: React.ReactNode, isLoading: boolean, isEditMode: boolean }) => { + + return
+
+ {children} + {isLoading && } +
+ + {/* edit button */} + {/* {role === 'user' && + { setIsEditMode(v => !v); }} + /> + } */} +
+} + + +const ChatBubble = ({ chatMessage, isLoading }: { chatMessage: ChatMessage, isLoading?: boolean, }) => { const role = chatMessage.role // edit mode state const [isEditMode, setIsEditMode] = useState(false) + + if (!chatMessage.content) { // don't show if empty + return null + } + let chatbubbleContents: React.ReactNode if (role === 'user') { chatbubbleContents = <> - + {chatMessage.displayContent} {/* {!isEditMode ? chatMessage.displayContent : <>} */} @@ -430,41 +518,9 @@ const ChatBubble = ({ chatMessage, isLoading }: { chatbubbleContents = } - return
-
- {chatbubbleContents} - {isLoading && } -
- - {/* edit button */} - {/* {role === 'user' && - { setIsEditMode(v => !v); }} - /> - } */} -
+ return + {chatbubbleContents} + } @@ -474,7 +530,8 @@ export const SidebarChat = () => { const textAreaFnsRef = useRef(null) const accessor = useAccessor() - const modelService = accessor.get('IModelService') + // const modelService = accessor.get('IModelService') + const commandService = accessor.get('ICommandService') // ----- HIGHER STATE ----- // sidebar state @@ -499,10 +556,10 @@ export const SidebarChat = () => { const selections = chatThreadsState.currentStagingSelections // stream state - const chatThreadsStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId) - const isCurrThreadStreaming = !!chatThreadsStreamState?.streamingToken - const latestError = chatThreadsStreamState?.error - const messageSoFar = chatThreadsStreamState?.messageSoFar + const currThreadStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId) + const isStreaming = !!currThreadStreamState?.streamingToken + const latestError = currThreadStreamState?.error + const messageSoFar = currThreadStreamState?.messageSoFar // ----- SIDEBAR CHAT state (local) ----- @@ -521,7 +578,7 @@ export const SidebarChat = () => { const onSubmit = async () => { if (isDisabled) return - if (isCurrThreadStreaming) return + if (isStreaming) return // send message to LLM const userMessage = textAreaRef.current?.value ?? '' @@ -534,9 +591,8 @@ export const SidebarChat = () => { } const onAbort = () => { - const token = chatThreadsStreamState?.streamingToken - if (!token) return - chatThreadsService.cancelStreaming(token) + const threadId = currentThread.id + chatThreadsService.cancelStreaming(threadId) } // const [_test_messages, _set_test_messages] = useState([]) @@ -566,11 +622,11 @@ export const SidebarChat = () => { scrollContainerRef={scrollContainerRef} className={` w-full h-auto - flex flex-col gap-0 + flex flex-col gap-1 overflow-x-hidden overflow-y-auto `} - style={{ maxHeight: sidebarDimensions.height - historyDimensions.height - formDimensions.height - 30 }} // the height of the previousMessages is determined by all other heights + style={{ maxHeight: sidebarDimensions.height - historyDimensions.height - formDimensions.height - 36 }} // the height of the previousMessages is determined by all other heights > {/* previous messages */} {previousMessages.map((message, i) => @@ -578,7 +634,22 @@ export const SidebarChat = () => { )} {/* message stream */} - + + + + {/* error message */} + {latestError === undefined ? null : +
+ { chatThreadsService.dismissStreamError(currentThread.id) }} + showDismiss={true} + /> + + { commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) }} text='Open settings' /> +
+ } @@ -590,7 +661,7 @@ export const SidebarChat = () => {
{ {/* top row */} <> {/* selections */} - {(selections && selections.length !== 0) && - - } - - {/* error message */} - {latestError === undefined ? null : - { chatThreadsService.dismissStreamError(currentThread.id) }} - showDismiss={true} - /> - } + {/* middle row */} @@ -625,7 +684,7 @@ export const SidebarChat = () => { {/* text input */} { setInstructionsAreEmpty(!newStr) }, [setInstructionsAreEmpty])} onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { @@ -653,7 +712,7 @@ export const SidebarChat = () => {
{/* submit / stop button */} - {isCurrThreadStreaming ? + {isStreaming ? // stop button { const { allThreads } = threadsState // sorted by most recent to least recent - const sortedThreadIds = Object.keys(allThreads ?? {}).sort((threadId1, threadId2) => allThreads![threadId1].lastModified > allThreads![threadId2].lastModified ? -1 : 1) + const sortedThreadIds = Object.keys(allThreads ?? {}) + .sort((threadId1, threadId2) => allThreads![threadId1].lastModified > allThreads![threadId2].lastModified ? -1 : 1) + .filter(threadId => allThreads![threadId].messages.length !== 0) return ( -
+
{/* title */} diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx index b5613ab6..b15cd2a6 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx @@ -10,6 +10,7 @@ import { IDisposable } from '../../../../../../../base/common/lifecycle.js' import { VoidSidebarState } from '../../../sidebarStateService.js' import { VoidSettingsState } from '../../../../../../../platform/void/common/voidSettingsService.js' import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js' +import { VoidUriState } from '../../../voidUriStateService.js'; import { VoidQuickEditState } from '../../../quickEditStateService.js' import { RefreshModelStateOfProvider } from '../../../../../../../platform/void/common/refreshModelService.js' @@ -28,6 +29,7 @@ import { ILLMMessageService } from '../../../../../../../platform/void/common/ll import { IRefreshModelService } from '../../../../../../../platform/void/common/refreshModelService.js'; import { IVoidSettingsService } from '../../../../../../../platform/void/common/voidSettingsService.js'; import { IInlineDiffsService } from '../../../inlineDiffsService.js'; +import { IVoidUriStateService } from '../../../voidUriStateService.js'; import { IQuickEditStateService } from '../../../quickEditStateService.js'; import { ISidebarStateService } from '../../../sidebarStateService.js'; import { IChatThreadService } from '../../../chatThreadService.js'; @@ -47,10 +49,14 @@ import { IPathService } from '../../../../../../../workbench/services/path/commo import { IMetricsService } from '../../../../../../../platform/void/common/metricsService.js' + // normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes // even if React hasn't mounted yet, the variables are always updated to the latest state. // React listens by adding a setState function to these listeners. +let uriState: VoidUriState +const uriStateListeners: Set<(s: VoidUriState) => void> = new Set() + let quickEditState: VoidQuickEditState const quickEditStateListeners: Set<(s: VoidQuickEditState) => void> = new Set() @@ -90,6 +96,7 @@ export const _registerServices = (accessor: ServicesAccessor) => { _registerAccessor(accessor) const stateServices = { + uriStateService: accessor.get(IVoidUriStateService), quickEditStateService: accessor.get(IQuickEditStateService), sidebarStateService: accessor.get(ISidebarStateService), chatThreadsStateService: accessor.get(IChatThreadService), @@ -99,7 +106,15 @@ export const _registerServices = (accessor: ServicesAccessor) => { inlineDiffsService: accessor.get(IInlineDiffsService), } - const { sidebarStateService, quickEditStateService, settingsStateService, chatThreadsStateService, refreshModelService, themeService, inlineDiffsService } = stateServices + const { uriStateService, sidebarStateService, quickEditStateService, settingsStateService, chatThreadsStateService, refreshModelService, themeService, inlineDiffsService } = stateServices + + uriState = uriStateService.state + disposables.push( + uriStateService.onDidChangeState(() => { + uriState = uriStateService.state + uriStateListeners.forEach(l => l(uriState)) + }) + ) quickEditState = quickEditStateService.state disposables.push( @@ -178,6 +193,7 @@ const getReactAccessor = (accessor: ServicesAccessor) => { IRefreshModelService: accessor.get(IRefreshModelService), IVoidSettingsService: accessor.get(IVoidSettingsService), IInlineDiffsService: accessor.get(IInlineDiffsService), + IVoidUriStateService: accessor.get(IVoidUriStateService), IQuickEditStateService: accessor.get(IQuickEditStateService), ISidebarStateService: accessor.get(ISidebarStateService), IChatThreadService: accessor.get(IChatThreadService), @@ -224,6 +240,16 @@ export const useAccessor = () => { // -- state of services -- +export const useUriState = () => { + const [s, ss] = useState(uriState) + useEffect(() => { + ss(uriState) + uriStateListeners.add(ss) + return () => { uriStateListeners.delete(ss) } + }, [ss]) + return s +} + export const useQuickEditState = () => { const [s, ss] = useState(quickEditState) useEffect(() => { diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index d33df07d..5787fc4d 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -26,10 +26,9 @@ import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { URI } from '../../../../base/common/uri.js'; import { localize2 } from '../../../../nls.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; - +import { IVoidUriStateService } from './voidUriStateService.js'; // ---------- Register commands and keybindings ---------- @@ -153,7 +152,15 @@ registerAction2(class extends Action2 { registerAction2(class extends Action2 { constructor() { - super({ id: VOID_CTRL_L_ACTION_ID, title: 'Void: Press Ctrl+L', keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyL, weight: KeybindingWeight.BuiltinExtension } }); + super({ + id: VOID_CTRL_L_ACTION_ID, + f1: true, + title: localize2('voidCtrlL', 'Void: Add Select to Chat'), + keybinding: { + primary: KeyMod.CtrlCmd | KeyCode.KeyL, + weight: KeybindingWeight.VoidExtension + } + }); } async run(accessor: ServicesAccessor): Promise { const commandService = accessor.get(ICommandService) @@ -233,7 +240,7 @@ registerAction2(class extends Action2 { export class TabSwitchListener extends Disposable { constructor( - onSwitchTab: (uri: URI) => void, + onSwitchTab: () => void, @ICodeEditorService private readonly _editorService: ICodeEditorService, ) { super() @@ -242,7 +249,7 @@ export class TabSwitchListener extends Disposable { const addTabSwitchListeners = (editor: ICodeEditor) => { this._register(editor.onDidChangeModel(e => { if (e.newModelUrl?.scheme !== 'file') return - onSwitchTab(e.newModelUrl) + onSwitchTab() })) } @@ -262,8 +269,10 @@ class TabSwitchContribution extends Disposable implements IWorkbenchContribution constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, - @ICommandService private readonly commandService: ICommandService, @IViewsService private readonly viewsService: IViewsService, + @IVoidUriStateService private readonly uriStateService: IVoidUriStateService, + @ICodeEditorService private readonly codeEditorService: ICodeEditorService, + // @ICommandService private readonly commandService: ICommandService, ) { super() @@ -273,18 +282,22 @@ class TabSwitchContribution extends Disposable implements IWorkbenchContribution sidebarIsVisible = e.visible })) - const addCurrentFileIfVisible = () => { - if (sidebarIsVisible) - this.commandService.executeCommand(VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID) + const onSwitchTab = () => { // update state + if (sidebarIsVisible) { + const currentUri = this.codeEditorService.getActiveCodeEditor()?.getModel()?.uri + if (!currentUri) return; + this.uriStateService.setState({ currentUri }) + // this.commandService.executeCommand(VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID) + } } // when sidebar becomes visible, add current file this._register(this.viewsService.onDidChangeViewVisibility(e => { sidebarIsVisible = e.visible })) // run on current tab if it exists, and listen for tab switches and visibility changes - addCurrentFileIfVisible() - this._register(this.viewsService.onDidChangeViewVisibility(() => { addCurrentFileIfVisible() })) - this._register(this.instantiationService.createInstance(TabSwitchListener, () => { addCurrentFileIfVisible() })) + onSwitchTab() + this._register(this.viewsService.onDidChangeViewVisibility(() => { onSwitchTab() })) + this._register(this.instantiationService.createInstance(TabSwitchListener, () => { onSwitchTab() })) } } diff --git a/src/vs/workbench/contrib/void/browser/voidUriStateService.ts b/src/vs/workbench/contrib/void/browser/voidUriStateService.ts new file mode 100644 index 00000000..1a89c29b --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/voidUriStateService.ts @@ -0,0 +1,57 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from '../../../../base/common/event.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { URI } from '../../../../base/common/uri.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; + + + +// service that manages state +export type VoidUriState = { + currentUri?: URI +} + +export interface IVoidUriStateService { + readonly _serviceBrand: undefined; + + readonly state: VoidUriState; // readonly to the user + setState(newState: Partial): void; + onDidChangeState: Event; +} + +export const IVoidUriStateService = createDecorator('voidUriStateService'); +class VoidUriStateService extends Disposable implements IVoidUriStateService { + _serviceBrand: undefined; + + static readonly ID = 'voidUriStateService'; + + private readonly _onDidChangeState = new Emitter(); + readonly onDidChangeState: Event = this._onDidChangeState.event; + + + // state + state: VoidUriState + + constructor( + ) { + super() + + // initial state + this.state = { currentUri: undefined } + } + + setState(newState: Partial) { + + this.state = { ...this.state, ...newState } + this._onDidChangeState.fire() + } + + +} + +registerSingleton(IVoidUriStateService, VoidUriStateService, InstantiationType.Eager); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css b/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css index 485075f3..454875dd 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css @@ -5,7 +5,7 @@ .file-icons-enabled .show-file-icons .vscode_getting_started_page-name-file-icon.file-icon::before { content: ' '; - background-image: url('../../../../browser/media/code-icon.svg'); + background-image: url('../../../../browser/media/void-icon-sm.png'); } .monaco-workbench .part.editor > .content .gettingStartedContainer { diff --git a/src/vs/workbench/contrib/welcomeWalkthrough/browser/media/walkThroughPart.css b/src/vs/workbench/contrib/welcomeWalkthrough/browser/media/walkThroughPart.css index 7ab127ea..8084ecec 100644 --- a/src/vs/workbench/contrib/welcomeWalkthrough/browser/media/walkThroughPart.css +++ b/src/vs/workbench/contrib/welcomeWalkthrough/browser/media/walkThroughPart.css @@ -113,7 +113,7 @@ .file-icons-enabled .show-file-icons .vs_code_editor_walkthrough\.md-name-file-icon.md-ext-file-icon.ext-file-icon.markdown-lang-file-icon.file-icon::before { content: ' '; - background-image: url('../../../../browser/media/code-icon.svg'); + background-image: url('../../../../browser/media/void-icon-sm.png'); } .monaco-workbench .part.editor > .content .walkThroughContent .mac-only,