diff --git a/src/vs/platform/void/electron-main/llmMessage/ollama.ts b/src/vs/platform/void/electron-main/llmMessage/ollama.ts index ac827ffe..a41eedb4 100644 --- a/src/vs/platform/void/electron-main/llmMessage/ollama.ts +++ b/src/vs/platform/void/electron-main/llmMessage/ollama.ts @@ -54,7 +54,8 @@ export const sendOllamaFIM: _InternalOllamaFIMMessageFnType = ({ messages, onTex suffix: messages.suffix, options: { stop: messages.stopTokens, - num_predict: 300 // max tokens + num_predict: 300, // max tokens + // repeat_penalty: 1, }, raw: true, stream: true, diff --git a/src/vs/workbench/contrib/void/browser/autocompleteService.ts b/src/vs/workbench/contrib/void/browser/autocompleteService.ts index 39957469..cb7f3099 100644 --- a/src/vs/workbench/contrib/void/browser/autocompleteService.ts +++ b/src/vs/workbench/contrib/void/browser/autocompleteService.ts @@ -8,7 +8,7 @@ import { ILanguageFeaturesService } from '../../../../editor/common/services/lan import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { ITextModel } from '../../../../editor/common/model.js'; import { Position } from '../../../../editor/common/core/position.js'; -import { DocumentSymbol, InlineCompletion, InlineCompletionContext, Location, } from '../../../../editor/common/languages.js'; +import { InlineCompletion, InlineCompletionContext, } from '../../../../editor/common/languages.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Range } from '../../../../editor/common/core/range.js'; import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js'; @@ -19,6 +19,7 @@ import { IModelService } from '../../../../editor/common/services/model.js'; import { extractCodeFromRegular } from './helpers/extractCodeFromResult.js'; import { isWindows } from '../../../../base/common/platform.js'; import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; +import { IContextGatheringService } from './contextGatheringService.js'; // The extension this was called from is here - https://github.com/voideditor/void/blob/autocomplete/extensions/void/src/extension/extension.ts @@ -746,11 +747,12 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ // gather relevant context from the code around the user's selection and definitions - const relevantContext = await this._gatherRelevantContextForPosition(model, position); + // const relevantSnippetsList = await this._contextGatheringService.readCachedSnippets(model, position, 3); + const relevantSnippetsList = this._contextGatheringService.getCachedSnippets(); + const relevantSnippets = relevantSnippetsList.map((text) => `${text}`).join('\n-------------------------------\n') + console.log('@@---------------------\n' + relevantSnippets) - console.log('@@---------------------\n' + relevantContext) - - const { shouldGenerate, predictionType, llmPrefix, llmSuffix, stopTokens } = getCompletionOptions(prefixAndSuffix, relevantContext, justAcceptedAutocompletion) + const { shouldGenerate, predictionType, llmPrefix, llmSuffix, stopTokens } = getCompletionOptions(prefixAndSuffix, relevantSnippets, justAcceptedAutocompletion) if (!shouldGenerate) return [] @@ -870,405 +872,12 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ } - // TODO! Given a user's cursor position, get relevant context. - // algorithm pseudocode: - - // 1. get all relevant symbols (functions, variables, and types) - // 1a. get all symbols that are `numNearbyLines` lines above and below the current position - // eg. if the context is this: - // ``` - // ... - // const addVectors = (a: Vector, b: Vector) => { - // - // ... 100+ LINES OF CODE - // return addVectorsElementWise(a,b, Math.min(a.length, b.length) as NumberType) [[CURSOR]] - // } - // ... - // ``` - // then these are all of the symbols it should consider that are above and below the position: ['addVectorsElementWise', 'Math.min', 'a.length', 'b.length', 'NumberType'] - - // 1b. look at where the parent function is defined and get its nearby symbols `numParentLines` - // ex. - // ``` - // ... - // const addVectors = (a: Vector, b: Vector) => { [[THIS IS THE PARENT FUNCTION]] - // ... 100+ LINES OF CODE - // return addVectorsElementWise(a ,b, Math.min(a.length, b.length)) [[CURSOR IS HERE]] - // } - // ... - // ``` - // the symbols of the parent function are ['const', 'addVectors', 'a', 'Vector', 'b', 'Vector'] - - - // 2. Cmd+Click on each symbol in step 1. (view instances and definitions) - // check that you don't visit the same place twice - // if this location is new, get `` lines above and below this new location and save that string to an array - - // 3. for each of the new positions found in step 2., use step 1 to find all their symbols again. This is the recursive step. - - // use `maxRecursionDepth` to prevent slowness - // set `numNearbyLines` and `numParentLines` to 2 after the first step to increase performance - - // 4. when finished, return snippets.join('\n----------------\n') - - private _docSymbolsCache: { - [docUri: string]: { - version: number; - symbols: DocumentSymbol[]; - }; - } = Object.create(null); - - // For each file, store per-symbol lookups we've done. - // e.g. _symbolLookupCache[docUri][fileVersion]["root"] => Location[] results - private _symbolLookupCache: { - [docUri: string]: { - [version: number]: { - [symbolName: string]: Location[]; - }; - }; - } = Object.create(null); - - private async _gatherRelevantContextForPosition( - model: ITextModel, - position: Position, - maxRecursionDepth: number = 3, - numNearbyLines: number = 5, - numParentLines: number = 5, - numSaveLines: number = 10 - ): Promise { - /**************************************************************************** - * A. Quick Helpers & caches - ****************************************************************************/ - type EditorLocation = import('vs/editor/common/languages').Location; - - const docUri = model.uri.toString(); - const fileVersion = model.getAlternativeVersionId(); - // If you prefer, do a text-based hash or use model.getVersionId() instead. - - // 1) Ensure docSymbols cache - let docSymCache = this._docSymbolsCache[docUri]; - if (!docSymCache || docSymCache.version !== fileVersion) { - docSymCache = { - version: fileVersion, - symbols: await this._getDocumentSymbolsOnce(model) // see helper below - }; - this._docSymbolsCache[docUri] = docSymCache; - } - const allDocumentSymbols = docSymCache.symbols; - - // 2) Ensure symbol lookup cache - if (!this._symbolLookupCache[docUri]) { - this._symbolLookupCache[docUri] = {}; - } - if (!this._symbolLookupCache[docUri][fileVersion]) { - this._symbolLookupCache[docUri][fileVersion] = {}; - } - const symbolLookupForFile = this._symbolLookupCache[docUri][fileVersion]; - - // Basic numeric clamps - const clampLine = (line: number): number => { - const maxLine = model.getLineCount(); - return Math.max(1, Math.min(line, maxLine)); - }; - - // Return a snippet of lines [start..end] in the document - const snippetForRange = (startLine: number, endLine: number): string => { - const lines: string[] = []; - for (let ln = startLine; ln <= endLine; ln++) { - lines.push(model.getLineContent(ln)); - } - return lines.join('\n'); - }; - - /**************************************************************************** - * B. Interval-based BFS to gather code blocks without duplication - ****************************************************************************/ - interface Interval { start: number; end: number; } - function addInterval(intervals: Interval[], start: number, end: number) { - // Merge new [start..end] with existing intervals if they overlap or touch - for (let i = 0; i < intervals.length; i++) { - const iv = intervals[i]; - if (!(end < iv.start - 1 || start > iv.end + 1)) { - // Overlaps (or touches); merge - const mergedStart = Math.min(iv.start, start); - const mergedEnd = Math.max(iv.end, end); - intervals.splice(i, 1); // remove old - addInterval(intervals, mergedStart, mergedEnd); // re-run - return; - } - } - intervals.push({ start, end }); - } - - function intervalsToString(intervals: Interval[]): string { - intervals.sort((a, b) => a.start - b.start); - return intervals - .map(iv => snippetForRange(iv.start, iv.end)) - .join('\n------------------------------\n'); - } - - const intervals: Interval[] = []; - const visitedRanges = new Set(); - - function markVisited(s: number, e: number) { visitedRanges.add(`${s}-${e}`); } - function isVisited(s: number, e: number) { return visitedRanges.has(`${s}-${e}`); } - - /**************************************************************************** - * C. Compute initial intervals (cursor region, parent symbol region) - ****************************************************************************/ - const lineNumber = position.lineNumber; - const localStart = clampLine(lineNumber - numNearbyLines); - const localEnd = clampLine(lineNumber + numNearbyLines); - - addInterval(intervals, clampLine(localStart - numSaveLines), clampLine(localEnd + numSaveLines)); - markVisited(localStart, localEnd); - - // get parent symbol, add interval for it - const parent = this._findEnclosingSymbol(allDocumentSymbols, lineNumber); - if (parent) { - const pStart = clampLine(parent.range.startLineNumber - numParentLines); - const pEnd = clampLine(parent.range.endLineNumber + numParentLines); - addInterval(intervals, pStart, pEnd); - markVisited(pStart, pEnd); - } - - /**************************************************************************** - * D. BFS data structures - ****************************************************************************/ - interface QItem { start: number; end: number; depth: number; } - const queue: QItem[] = []; - - queue.push({ start: localStart, end: localEnd, depth: 1 }); - if (parent) { - const pStart = clampLine(parent.range.startLineNumber - numParentLines); - const pEnd = clampLine(parent.range.endLineNumber + numParentLines); - queue.push({ start: pStart, end: pEnd, depth: 1 }); - } - - // We'll keep a set of symbols we've done "references + definitions" for: - const visitedSymbolNames = new Set(); - - // Providers - const definitionProviders = this._langFeatureService.definitionProvider.ordered(model); - const referenceProviders = this._langFeatureService.referenceProvider.ordered(model); - - /**************************************************************************** - * E. BFS Loop - ****************************************************************************/ - while (queue.length) { - const { start, end, depth } = queue.shift()!; - if (depth >= maxRecursionDepth) continue; - - // Step 1: Gather all symbols in [start..end] - const regionSyms = this._gatherSymbolsInLineRange(allDocumentSymbols, start, end); - - // For each symbol, do references/defs once per symbol name - for (const sym of regionSyms) { - // If we already resolved that symbolName, skip - const symName = sym.name || ''; - if (!symName) continue; - if (visitedSymbolNames.has(symName)) continue; - visitedSymbolNames.add(symName); - - // If symbol was cached before, skip re-resolving references - if (symbolLookupForFile[symName]) { - // We already have references/definitions => merge them into intervals - const existingLocs = symbolLookupForFile[symName]; - for (const loc of existingLocs) { - const rng = loc.range; - const locStart = clampLine(rng.startLineNumber - numSaveLines); - const locEnd = clampLine(rng.endLineNumber + numSaveLines); - if (!isVisited(locStart, locEnd)) { - markVisited(locStart, locEnd); - addInterval(intervals, locStart, locEnd); - queue.push({ start: locStart, end: locEnd, depth: depth + 1 }); - } - } - continue; - } - - // Not cached => actually ask definitionProviders / referenceProviders - const symPos = this._symbolPosition(sym); // see helper below - let foundLocs: EditorLocation[] = []; - - for (const dp of definitionProviders) { - try { - const defs = await dp.provideDefinition(model, symPos, CancellationToken.None); - if (defs) foundLocs.push(...(Array.isArray(defs) ? defs : [defs])); - } catch {/* ignore */ } - } - for (const rp of referenceProviders) { - try { - const refs = await rp.provideReferences( - model, symPos, { includeDeclaration: true }, CancellationToken.None - ); - if (refs) foundLocs.push(...refs); - } catch {/* ignore */ } - } - - // Filter same-file only - foundLocs = foundLocs.filter(loc => loc.uri.toString() === docUri); - - // Cache them - symbolLookupForFile[symName] = foundLocs; - - // Enqueue each discovered reference/definition - for (const loc of foundLocs) { - const rng = loc.range; - const locStart = clampLine(rng.startLineNumber - numSaveLines); - const locEnd = clampLine(rng.endLineNumber + numSaveLines); - if (!isVisited(locStart, locEnd)) { - markVisited(locStart, locEnd); - addInterval(intervals, locStart, locEnd); - queue.push({ start: locStart, end: locEnd, depth: depth + 1 }); - } - } - } - - // Step 2: Also do naive token-scan for lines in [start..end], - // so e.g. 'root()' calls get recognized if not in docSymbols. - // We can do basically the same "cache symbol name" logic, if you want: - for (let ln = start; ln <= end; ln++) { - const text = model.getLineContent(ln); - const tokens = text.match(/[a-zA-Z_][a-zA-Z0-9_]*/g) || []; - for (const token of tokens) { - if (visitedSymbolNames.has(token)) continue; - visitedSymbolNames.add(token); - - // If cached, merge intervals from cache - if (symbolLookupForFile[token]) { - for (const loc of symbolLookupForFile[token]) { - const rng = loc.range; - const locStart = clampLine(rng.startLineNumber - numSaveLines); - const locEnd = clampLine(rng.endLineNumber + numSaveLines); - if (!isVisited(locStart, locEnd)) { - markVisited(locStart, locEnd); - addInterval(intervals, locStart, locEnd); - queue.push({ start: locStart, end: locEnd, depth: depth + 1 }); - } - } - continue; - } - - // Actually compute definitions/references - const colIdx = text.indexOf(token); - if (colIdx < 0) continue; // should not happen, but just in case - const tokenPos = new Position(ln, colIdx + 1); - let foundLocs: EditorLocation[] = []; - - for (const dp of definitionProviders) { - try { - const defs = await dp.provideDefinition(model, tokenPos, CancellationToken.None); - if (defs) foundLocs.push(...(Array.isArray(defs) ? defs : [defs])); - } catch {/* ignore */ } - } - for (const rp of referenceProviders) { - try { - const refs = await rp.provideReferences( - model, tokenPos, { includeDeclaration: true }, CancellationToken.None - ); - if (refs) foundLocs.push(...refs); - } catch {/* ignore */ } - } - foundLocs = foundLocs.filter(loc => loc.uri.toString() === docUri); - - // Cache them - symbolLookupForFile[token] = foundLocs; - - // Add intervals - for (const loc of foundLocs) { - const rng = loc.range; - const locStart = clampLine(rng.startLineNumber - numSaveLines); - const locEnd = clampLine(rng.endLineNumber + numSaveLines); - if (!isVisited(locStart, locEnd)) { - markVisited(locStart, locEnd); - addInterval(intervals, locStart, locEnd); - queue.push({ start: locStart, end: locEnd, depth: depth + 1 }); - } - } - } - } - } - - /**************************************************************************** - * F. Finally, merge intervals and produce final snippet - ****************************************************************************/ - return intervalsToString(intervals); - } - - - /****************************************************************************** - * Additional Helpers - ******************************************************************************/ - private async _getDocumentSymbolsOnce(model: ITextModel): Promise { - const providers = this._langFeatureService.documentSymbolProvider.ordered(model); - let result: DocumentSymbol[] = []; - for (const p of providers) { - try { - const syms = await p.provideDocumentSymbols(model, CancellationToken.None); - if (syms) { - result.push(...syms); - } - } catch {/* ignore */ } - } - return result; - } - - private _findEnclosingSymbol(symbols: DocumentSymbol[], line: number): DocumentSymbol | undefined { - for (const s of symbols) { - if (s.range.startLineNumber <= line && s.range.endLineNumber >= line) { - // Recurse deeper - const child = this._findEnclosingSymbol(s.children || [], line); - return child || s; - } - } - return undefined; - } - - private _symbolPosition(ds: DocumentSymbol): Position { - return new Position(ds.selectionRange.startLineNumber, ds.selectionRange.startColumn); - } - - private _gatherSymbolsInLineRange( - symbols: DocumentSymbol[], - startLine: number, - endLine: number - ): DocumentSymbol[] { - const out: DocumentSymbol[] = []; - for (const ds of symbols) { - if (ds.range.endLineNumber >= startLine && ds.range.startLineNumber <= endLine) { - out.push(ds); - } - if (ds.children?.length) { - out.push(...this._gatherSymbolsInLineRange(ds.children, startLine, endLine)); - } - } - return out; - } - - - - - - - - - - - - - - - - - - - constructor( @ILanguageFeaturesService private _langFeatureService: ILanguageFeaturesService, @ILLMMessageService private readonly _llmMessageService: ILLMMessageService, @IEditorService private readonly _editorService: IEditorService, @IModelService private readonly _modelService: IModelService, + @IContextGatheringService private readonly _contextGatheringService: IContextGatheringService, ) { super() diff --git a/src/vs/workbench/contrib/void/browser/contextGatheringService.ts b/src/vs/workbench/contrib/void/browser/contextGatheringService.ts new file mode 100644 index 00000000..c20e8fb8 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/contextGatheringService.ts @@ -0,0 +1,354 @@ +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { Position } from '../../../../editor/common/core/position.js'; +import { DocumentSymbol, SymbolKind } from '../../../../editor/common/languages.js'; +import { ITextModel } from '../../../../editor/common/model.js'; +import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { Range, IRange } from '../../../../editor/common/core/range.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { IModelService } from '../../../../editor/common/services/model.js'; +import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; +import { URI } from '../../../../base/common/uri.js'; + + +// make sure snippet logic works +// change logic for `visited` to intervals +// atomically set new snippets at end +// throttle cache setting + +interface IVisitedInterval { + uri: string; + startLine: number; + endLine: number; +} + +export interface IContextGatheringService { + readonly _serviceBrand: undefined; + updateCache(model: ITextModel, pos: Position): Promise; + getCachedSnippets(): string[]; +} + +export const IContextGatheringService = createDecorator('contextGatheringService'); + +class ContextGatheringService extends Disposable implements IContextGatheringService { + _serviceBrand: undefined; + private readonly _NUM_LINES = 3; + private readonly _MAX_SNIPPET_LINES = 7; // Reasonable size for context + // Cache holds the most recent list of snippets. + private _cache: string[] = []; + private _snippetIntervals: IVisitedInterval[] = []; + + constructor( + @ILanguageFeaturesService private readonly _langFeaturesService: ILanguageFeaturesService, + @IModelService private readonly _modelService: IModelService, + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService + ) { + super(); + this._modelService.getModels().forEach(model => this._subscribeToModel(model)); + this._register(this._modelService.onModelAdded(model => this._subscribeToModel(model))); + } + + private _subscribeToModel(model: ITextModel): void { + console.log("Subscribing to model:", model.uri.toString()); + this._register(model.onDidChangeContent(() => { + const editor = this._codeEditorService.getFocusedCodeEditor(); + if (editor && editor.getModel() === model) { + const pos = editor.getPosition(); + console.log("updateCache called at position:", pos); + if (pos) { + this.updateCache(model, pos); + } + } + })); + } + + public async updateCache(model: ITextModel, pos: Position): Promise { + const snippets = new Set(); + this._snippetIntervals = []; // Reset intervals for new cache update + + await this._gatherNearbySnippets(model, pos, this._NUM_LINES, 3, snippets, this._snippetIntervals); + await this._gatherParentSnippets(model, pos, this._NUM_LINES, 3, snippets, this._snippetIntervals); + + // Convert to array and filter overlapping snippets + this._cache = Array.from(snippets); + console.log("Cache updated:", this._cache); + } + + public getCachedSnippets(): string[] { + return this._cache; + } + + // Basic snippet extraction. + private _getSnippetForRange(model: ITextModel, range: IRange, numLines: number): string { + const startLine = Math.max(range.startLineNumber - numLines, 1); + const endLine = Math.min(range.endLineNumber + numLines, model.getLineCount()); + + // Enforce maximum snippet size + const totalLines = endLine - startLine + 1; + const adjustedStartLine = totalLines > this._MAX_SNIPPET_LINES + ? endLine - this._MAX_SNIPPET_LINES + 1 + : startLine; + + const snippetRange = new Range(adjustedStartLine, 1, endLine, model.getLineMaxColumn(endLine)); + return this._cleanSnippet(model.getValueInRange(snippetRange)); + } + + private _cleanSnippet(snippet: string): string { + return snippet + .split('\n') + // Remove empty lines and lines with only comments + .filter(line => { + const trimmed = line.trim(); + return trimmed && !/^\/\/+$/.test(trimmed); + }) + // Rejoin with newlines + .join('\n') + // Remove excess whitespace + .trim(); + } + + private _normalizeSnippet(snippet: string): string { + return snippet + // Remove multiple newlines + .replace(/\n{2,}/g, '\n') + // Remove trailing whitespace + .trim(); + } + + private _addSnippetIfNotOverlapping( + model: ITextModel, + range: IRange, + snippets: Set, + visited: IVisitedInterval[] + ): void { + const startLine = range.startLineNumber; + const endLine = range.endLineNumber; + const uri = model.uri.toString(); + + if (!this._isRangeVisited(uri, startLine, endLine, visited)) { + visited.push({ uri, startLine, endLine }); + const snippet = this._normalizeSnippet(this._getSnippetForRange(model, range, this._NUM_LINES)); + if (snippet.length > 0) { + snippets.add(snippet); + } + } + } + + private async _gatherNearbySnippets( + model: ITextModel, + pos: Position, + numLines: number, + depth: number, + snippets: Set, + visited: IVisitedInterval[] + ): Promise { + if (depth <= 0) return; + + const startLine = Math.max(pos.lineNumber - numLines, 1); + const endLine = Math.min(pos.lineNumber + numLines, model.getLineCount()); + const range = new Range(startLine, 1, endLine, model.getLineMaxColumn(endLine)); + + this._addSnippetIfNotOverlapping(model, range, snippets, visited); + + const symbols = await this._getSymbolsNearPosition(model, pos, numLines); + for (const sym of symbols) { + const defs = await this._getDefinitionSymbols(model, sym); + for (const def of defs) { + const defModel = this._modelService.getModel(def.uri); + if (defModel) { + const defPos = new Position(def.range.startLineNumber, def.range.startColumn); + this._addSnippetIfNotOverlapping(defModel, def.range, snippets, visited); + await this._gatherNearbySnippets(defModel, defPos, numLines, depth - 1, snippets, visited); + } + } + } + } + + private async _gatherParentSnippets( + model: ITextModel, + pos: Position, + numLines: number, + depth: number, + snippets: Set, + visited: IVisitedInterval[] + ): Promise { + if (depth <= 0) return; + + const container = await this._findContainerFunction(model, pos); + if (!container) return; + + const containerRange = container.kind === SymbolKind.Method ? container.selectionRange : container.range; + this._addSnippetIfNotOverlapping(model, containerRange, snippets, visited); + + const symbols = await this._getSymbolsNearRange(model, containerRange, numLines); + for (const sym of symbols) { + const defs = await this._getDefinitionSymbols(model, sym); + for (const def of defs) { + const defModel = this._modelService.getModel(def.uri); + if (defModel) { + const defPos = new Position(def.range.startLineNumber, def.range.startColumn); + this._addSnippetIfNotOverlapping(defModel, def.range, snippets, visited); + await this._gatherNearbySnippets(defModel, defPos, numLines, depth - 1, snippets, visited); + } + } + } + + const containerPos = new Position(containerRange.startLineNumber, containerRange.startColumn); + await this._gatherParentSnippets(model, containerPos, numLines, depth - 1, snippets, visited); + } + + private _isRangeVisited(uri: string, startLine: number, endLine: number, visited: IVisitedInterval[]): boolean { + return visited.some(interval => + interval.uri === uri && + !(endLine < interval.startLine || startLine > interval.endLine) + ); + } + + private async _getSymbolsNearPosition(model: ITextModel, pos: Position, numLines: number): Promise { + const startLine = Math.max(pos.lineNumber - numLines, 1); + const endLine = Math.min(pos.lineNumber + numLines, model.getLineCount()); + const range = new Range(startLine, 1, endLine, model.getLineMaxColumn(endLine)); + return this._getSymbolsInRange(model, range); + } + + private async _getSymbolsNearRange(model: ITextModel, range: IRange, numLines: number): Promise { + const centerLine = Math.floor((range.startLineNumber + range.endLineNumber) / 2); + const startLine = Math.max(centerLine - numLines, 1); + const endLine = Math.min(centerLine + numLines, model.getLineCount()); + const searchRange = new Range(startLine, 1, endLine, model.getLineMaxColumn(endLine)); + return this._getSymbolsInRange(model, searchRange); + } + + private async _getSymbolsInRange(model: ITextModel, range: IRange): Promise { + const symbols: DocumentSymbol[] = []; + const providers = this._langFeaturesService.documentSymbolProvider.ordered(model); + for (const provider of providers) { + try { + const result = await provider.provideDocumentSymbols(model, CancellationToken.None); + if (result) { + const flat = this._flattenSymbols(result); + const intersecting = flat.filter(sym => this._rangesIntersect(sym.range, range)); + symbols.push(...intersecting); + } + } catch (e) { + console.warn("Symbol provider error:", e); + } + } + // Also check reference providers. + const refProviders = this._langFeaturesService.referenceProvider.ordered(model); + for (let line = range.startLineNumber; line <= range.endLineNumber; line++) { + const content = model.getLineContent(line); + const words = content.match(/[a-zA-Z_]\w*/g) || []; + for (const word of words) { + const startColumn = content.indexOf(word) + 1; + const pos = new Position(line, startColumn); + if (!this._positionInRange(pos, range)) continue; + for (const provider of refProviders) { + try { + const refs = await provider.provideReferences(model, pos, { includeDeclaration: true }, CancellationToken.None); + if (refs) { + const filtered = refs.filter(ref => this._rangesIntersect(ref.range, range)); + for (const ref of filtered) { + symbols.push({ + name: word, + detail: '', + kind: SymbolKind.Variable, + range: ref.range, + selectionRange: ref.range, + children: [], + tags: [] + }); + } + } + } catch (e) { + console.warn("Reference provider error:", e); + } + } + } + } + return symbols; + } + + private _flattenSymbols(symbols: DocumentSymbol[]): DocumentSymbol[] { + const flat: DocumentSymbol[] = []; + for (const sym of symbols) { + flat.push(sym); + if (sym.children && sym.children.length > 0) { + flat.push(...this._flattenSymbols(sym.children)); + } + } + return flat; + } + + private _rangesIntersect(a: IRange, b: IRange): boolean { + return !( + a.endLineNumber < b.startLineNumber || + a.startLineNumber > b.endLineNumber || + (a.endLineNumber === b.startLineNumber && a.endColumn < b.startColumn) || + (a.startLineNumber === b.endLineNumber && a.endColumn > b.endColumn) + ); + } + + private _positionInRange(pos: Position, range: IRange): boolean { + return pos.lineNumber >= range.startLineNumber && + pos.lineNumber <= range.endLineNumber && + (pos.lineNumber !== range.startLineNumber || pos.column >= range.startColumn) && + (pos.lineNumber !== range.endLineNumber || pos.column <= range.endColumn); + } + + // Get definition symbols for a given symbol. + private async _getDefinitionSymbols(model: ITextModel, symbol: DocumentSymbol): Promise<(DocumentSymbol & { uri: URI })[]> { + const pos = new Position(symbol.range.startLineNumber, symbol.range.startColumn); + const providers = this._langFeaturesService.definitionProvider.ordered(model); + const defs: (DocumentSymbol & { uri: URI })[] = []; + for (const provider of providers) { + try { + const res = await provider.provideDefinition(model, pos, CancellationToken.None); + if (res) { + const links = Array.isArray(res) ? res : [res]; + defs.push(...links.map(link => ({ + name: symbol.name, + detail: symbol.detail, + kind: symbol.kind, + range: link.range, + selectionRange: link.range, + children: [], + tags: symbol.tags || [], + uri: link.uri // Now keeping it as URI instead of converting to string + }))); + } + } catch (e) { + console.warn("Definition provider error:", e); + } + } + return defs; + } + + private async _findContainerFunction(model: ITextModel, pos: Position): Promise { + const searchRange = new Range( + Math.max(pos.lineNumber - 1, 1), 1, + Math.min(pos.lineNumber + 1, model.getLineCount()), + model.getLineMaxColumn(pos.lineNumber) + ); + const symbols = await this._getSymbolsInRange(model, searchRange); + const funcs = symbols.filter(s => + (s.kind === SymbolKind.Function || s.kind === SymbolKind.Method) && + this._positionInRange(pos, s.range) + ); + if (!funcs.length) return null; + return funcs.reduce((innermost, current) => { + if (!innermost) return current; + const moreInner = + (current.range.startLineNumber > innermost.range.startLineNumber || + (current.range.startLineNumber === innermost.range.startLineNumber && + current.range.startColumn > innermost.range.startColumn)) && + (current.range.endLineNumber < innermost.range.endLineNumber || + (current.range.endLineNumber === innermost.range.endLineNumber && + current.range.endColumn < innermost.range.endColumn)); + return moreInner ? current : innermost; + }, null as DocumentSymbol | null); + } +} + +registerSingleton(IContextGatheringService, ContextGatheringService, InstantiationType.Eager); diff --git a/src/vs/workbench/contrib/void/browser/test.ts b/src/vs/workbench/contrib/void/browser/test.ts new file mode 100644 index 00000000..c34d1edf --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/test.ts @@ -0,0 +1,258 @@ +const root = () => { } + +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// + + + + + + +type ChangeType = { name: string; age: number }; + + + +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// + + + + + + +type EditType = { name: string; age: number }; + + + + +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// + +const fn = (params: { edit: EditType, change: ChangeType }) => { + + + + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + + + // + const r = root() + + + + + + + + + + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + + + + + + + +} + + + +const testVar = 5; + + + + +class MyClass { + + + + + + + + + + + + + + + + + + + + + + private test1(t: EditType) { + // + // + // + //// + // + // + //// + // + // + //// + // + // + //// + // + // + //// + // + // + //// + // + // + //// + // + // + //// + // + // + //// + // + // + //// + //x + // + // + + + const x = 1 + + + + + + + } +} + + +const diff --git a/src/vs/workbench/contrib/void/browser/void.contribution.ts b/src/vs/workbench/contrib/void/browser/void.contribution.ts index ebb17358..da87ac17 100644 --- a/src/vs/workbench/contrib/void/browser/void.contribution.ts +++ b/src/vs/workbench/contrib/void/browser/void.contribution.ts @@ -21,6 +21,10 @@ import './chatThreadService.js' // register Autocomplete import './autocompleteService.js' +// register Context services +import './contextGatheringService.js' +import './contextUserChangesService.js' + // settings pane import './voidSettingsPane.js'