33 * SPDX-License-Identifier: Apache-2.0
44 */
55import * as path from 'path'
6- import { Event as VSCodeEvent , Uri } from 'vscode'
6+ import { Event as VSCodeEvent , Uri , workspace , window , ViewColumn , Position , Selection } from 'vscode'
77import { EditorContextExtractor } from '../../editor/context/extractor'
88import { ChatSessionStorage } from '../../storages/chatSession'
99import { Messenger , MessengerResponseType , StaticTextResponseType } from './messenger/messenger'
@@ -28,6 +28,8 @@ import {
2828 ViewDiff ,
2929 AcceptDiff ,
3030 QuickCommandGroupActionClick ,
31+ MergedRelevantDocument ,
32+ FileClick ,
3133} from './model'
3234import {
3335 AppToWebViewMessageDispatcher ,
@@ -41,7 +43,7 @@ import { EditorContextCommand } from '../../commands/registerCommands'
4143import { PromptsGenerator } from './prompts/promptsGenerator'
4244import { TriggerEventsStorage } from '../../storages/triggerEvents'
4345import { SendMessageRequest } from '@amzn/amazon-q-developer-streaming-client'
44- import { CodeWhispererStreamingServiceException } from '@amzn/codewhisperer-streaming'
46+ import { CodeWhispererStreamingServiceException , RelevantTextDocument } from '@amzn/codewhisperer-streaming'
4547import { UserIntentRecognizer } from './userIntent/userIntentRecognizer'
4648import { CWCTelemetryHelper , recordTelemetryChatRunCommand } from './telemetryHelper'
4749import { CodeWhispererTracker } from '../../../codewhisperer/tracker/codewhispererTracker'
@@ -90,6 +92,7 @@ export interface ChatControllerMessagePublishers {
9092 readonly processQuickCommandGroupActionClicked : MessagePublisher < QuickCommandGroupActionClick >
9193 readonly processCustomFormAction : MessagePublisher < CustomFormActionMessage >
9294 readonly processContextSelected : MessagePublisher < ContextSelectedMessage >
95+ readonly processFileClick : MessagePublisher < FileClick >
9396}
9497
9598export interface ChatControllerMessageListeners {
@@ -114,6 +117,7 @@ export interface ChatControllerMessageListeners {
114117 readonly processQuickCommandGroupActionClicked : MessageListener < QuickCommandGroupActionClick >
115118 readonly processCustomFormAction : MessageListener < CustomFormActionMessage >
116119 readonly processContextSelected : MessageListener < ContextSelectedMessage >
120+ readonly processFileClick : MessageListener < FileClick >
117121}
118122
119123export class ChatController {
@@ -243,6 +247,9 @@ export class ChatController {
243247 this . chatControllerMessageListeners . processContextSelected . onMessage ( ( data ) => {
244248 return this . processContextSelected ( data )
245249 } )
250+ this . chatControllerMessageListeners . processFileClick . onMessage ( ( data ) => {
251+ return this . processFileClickMessage ( data )
252+ } )
246253 }
247254
248255 private processFooterInfoLinkClick ( click : FooterInfoLinkClick ) {
@@ -524,6 +531,7 @@ export class ChatController {
524531 `Create a saved prompt`
525532 )
526533 }
534+
527535 private processQuickCommandGroupActionClicked ( message : QuickCommandGroupActionClick ) {
528536 if ( message . actionId === 'create-prompt' ) {
529537 this . handlePromptCreate ( message . tabID )
@@ -558,6 +566,36 @@ export class ChatController {
558566 this . handlePromptCreate ( message . tabID )
559567 }
560568 }
569+ private async processFileClickMessage ( message : FileClick ) {
570+ let session = this . sessionStorage . getSession ( message . tabID )
571+ // TODO remove currentContextId but use messageID to track context for each answer message
572+ const lineRanges = session . contexts . get ( session . currentContextId ) ?. get ( message . filePath )
573+
574+ if ( ! lineRanges ) {
575+ return
576+ }
577+ const projectRoot = workspace . workspaceFolders ?. [ 0 ] ?. uri . fsPath
578+ if ( ! projectRoot ) {
579+ return
580+ }
581+
582+ const absoluteFilePath = path . join ( projectRoot , message . filePath )
583+
584+ // Open the file in VSCode
585+ const document = await workspace . openTextDocument ( absoluteFilePath )
586+ const editor = await window . showTextDocument ( document , ViewColumn . Active )
587+
588+ // Create multiple selections based on line ranges
589+ const selections : Selection [ ] = lineRanges . map ( ( { first, second } ) => {
590+ const startPosition = new Position ( first - 1 , 0 ) // Convert 1-based to 0-based
591+ const endPosition = new Position ( second - 1 , document . lineAt ( second - 1 ) . range . end . character )
592+ return new Selection ( startPosition . line , startPosition . character , endPosition . line , endPosition . character )
593+ } )
594+
595+ // Apply multiple selections to the editor using the new API
596+ editor . selection = selections [ 0 ] // Set the first selection as active
597+ editor . selections = selections // Apply multiple selections
598+ }
561599
562600 private processException ( e : any , tabID : string ) {
563601 let errorMessage = ''
@@ -880,9 +918,12 @@ export class ChatController {
880918 if ( CodeWhispererSettings . instance . isLocalIndexEnabled ( ) ) {
881919 const start = performance . now ( )
882920 triggerPayload . relevantTextDocuments = await LspController . instance . query ( triggerPayload . message )
921+ triggerPayload . mergedRelevantDocuments = this . mergeRelevantTextDocuments (
922+ triggerPayload . relevantTextDocuments
923+ )
883924 for ( const doc of triggerPayload . relevantTextDocuments ) {
884925 getLogger ( ) . info (
885- `amazonq: Using workspace files ${ doc . relativeFilePath } , content(partial): ${ doc . text ?. substring ( 0 , 200 ) } `
926+ `amazonq: Using workspace files ${ doc . relativeFilePath } , content(partial): ${ doc . text ?. substring ( 0 , 200 ) } , start line: ${ doc . startLine } , end line: ${ doc . endLine } `
886927 )
887928 }
888929 triggerPayload . projectContextQueryLatencyMs = performance . now ( ) - start
@@ -904,12 +945,25 @@ export class ChatController {
904945 } ,
905946 { timeout : 500 , interval : 200 , truthy : false }
906947 )
948+ triggerPayload . mergedRelevantDocuments = this . mergeRelevantTextDocuments (
949+ triggerPayload . relevantTextDocuments
950+ )
907951 triggerPayload . projectContextQueryLatencyMs = performance . now ( ) - start
908952 }
909953 }
910954
911955 const request = triggerPayloadToChatRequest ( triggerPayload )
912956 const session = this . sessionStorage . getSession ( tabID )
957+
958+ session . currentContextId ++
959+ session . contexts . set ( session . currentContextId , new Map ( ) )
960+ triggerPayload . mergedRelevantDocuments ?. forEach ( ( doc ) => {
961+ const currentContext = session . contexts . get ( session . currentContextId )
962+ if ( currentContext ) {
963+ currentContext . set ( doc . relativeFilePath , doc . lineRanges )
964+ }
965+ } )
966+
913967 getLogger ( ) . info (
914968 `request from tab: ${ tabID } conversationID: ${ session . sessionIdentifier } request: ${ inspect ( request , {
915969 depth : 12 ,
@@ -918,7 +972,7 @@ export class ChatController {
918972 let response : MessengerResponseType | undefined = undefined
919973 session . createNewTokenSource ( )
920974 try {
921- this . messenger . sendInitalStream ( tabID , triggerID )
975+ this . messenger . sendInitalStream ( tabID , triggerID , triggerPayload . mergedRelevantDocuments )
922976 this . telemetryHelper . setConversationStreamStartTime ( tabID )
923977 if ( isSsoConnection ( AuthUtil . instance . conn ) ) {
924978 const { $metadata, generateAssistantResponseResponse } = await session . chatSso ( request )
@@ -948,4 +1002,44 @@ export class ChatController {
9481002 this . processException ( e , tabID )
9491003 }
9501004 }
1005+
1006+ private mergeRelevantTextDocuments (
1007+ documents : RelevantTextDocument [ ] | undefined
1008+ ) : MergedRelevantDocument [ ] | undefined {
1009+ if ( documents === undefined ) {
1010+ return undefined
1011+ }
1012+ return Object . entries (
1013+ documents . reduce < Record < string , { first : number ; second : number } [ ] > > ( ( acc , doc ) => {
1014+ if ( ! doc . relativeFilePath || doc . startLine === undefined || doc . endLine === undefined ) {
1015+ return acc // Skip invalid documents
1016+ }
1017+
1018+ if ( ! acc [ doc . relativeFilePath ] ) {
1019+ acc [ doc . relativeFilePath ] = [ ]
1020+ }
1021+ acc [ doc . relativeFilePath ] . push ( { first : doc . startLine , second : doc . endLine } )
1022+ return acc
1023+ } , { } )
1024+ ) . map ( ( [ filePath , ranges ] ) => {
1025+ // Sort by startLine
1026+ const sortedRanges = ranges . sort ( ( a , b ) => a . first - b . first )
1027+
1028+ const mergedRanges : { first : number ; second : number } [ ] = [ ]
1029+ for ( const { first, second } of sortedRanges ) {
1030+ if ( mergedRanges . length === 0 || mergedRanges [ mergedRanges . length - 1 ] . second < first - 1 ) {
1031+ // If no overlap, add new range
1032+ mergedRanges . push ( { first, second } )
1033+ } else {
1034+ // Merge overlapping or consecutive ranges
1035+ mergedRanges [ mergedRanges . length - 1 ] . second = Math . max (
1036+ mergedRanges [ mergedRanges . length - 1 ] . second ,
1037+ second
1038+ )
1039+ }
1040+ }
1041+
1042+ return { relativeFilePath : filePath , lineRanges : mergedRanges }
1043+ } )
1044+ }
9511045}
0 commit comments