Skip to content

Commit a96651e

Browse files
authored
fix: isInWorkspace should work on closed files. (#1004)
* fix: inWorkspace only worked on open files * refactor: inject lsp instead of workspaceFolders * fix: use proper workspace path in tests * fix: stub lsp initializeParams in exec bash * fix: isInWorkspace returns true for workspace itself
1 parent c2b7c25 commit a96651e

File tree

7 files changed

+80
-19
lines changed

7 files changed

+80
-19
lines changed

core/aws-lsp-core/src/util/workspaceUtils.test.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,57 @@ import * as fs from 'fs/promises'
33
import * as assert from 'assert'
44
import * as path from 'path'
55
import { TestFolder } from '../test/testFolder'
6-
import { readDirectoryRecursively, getEntryPath } from './workspaceUtils'
6+
import { readDirectoryRecursively, getEntryPath, isParentFolder, isInWorkspace } from './workspaceUtils'
77
import { TestFeatures } from '@aws/language-server-runtimes/testing'
88

99
describe('workspaceUtils', function () {
1010
beforeEach(function () {
1111
mockfs.restore()
1212
})
1313

14+
describe('isParentFolder', function () {
15+
it('handles different cases', function () {
16+
assert.ok(isParentFolder('/foo', '/foo/bar'), 'simple case')
17+
assert.ok(isParentFolder('/foo', '/foo/bar/'), 'trailing slash in child')
18+
assert.ok(isParentFolder('/foo', '/foo/bar.txt'), 'files')
19+
assert.ok(isParentFolder('/foo', '/foo/bar/baz'), 'neseted directory')
20+
assert.ok(!isParentFolder('/foo', '/foo'), 'duplicates')
21+
assert.ok(!isParentFolder('/foo', '/foobar'), 'prefixing')
22+
assert.ok(!isParentFolder('/foo/bar', '/foo'), 'reverse')
23+
assert.ok(isParentFolder('/foo/', '/foo/bar/baz/'), 'trailing slash in both')
24+
})
25+
})
26+
27+
describe('isInWorkspace', function () {
28+
it('finds the file within the workspace', function () {
29+
const workspaceFolders = ['/foo']
30+
31+
const positiveFilePath = '/foo/bar/baz.txt'
32+
const negativeFilePath = '/notfoo/bar/baz.txt'
33+
34+
assert.ok(isInWorkspace(workspaceFolders, positiveFilePath), 'file is within the workspace')
35+
assert.ok(!isInWorkspace(workspaceFolders, negativeFilePath), 'file is not within the workspace')
36+
})
37+
38+
it('handles multi-root workspaces', function () {
39+
const workspaceFolders = ['/foo', '/bar']
40+
41+
const positiveFilePath = '/foo/bar/baz.txt'
42+
const secondPositiveFilePath = '/bar/bax.txt'
43+
const negativeFilePath = '/notfoo/bar/baz.txt'
44+
45+
assert.ok(isInWorkspace(workspaceFolders, positiveFilePath), 'file is within the workspace')
46+
assert.ok(isInWorkspace(workspaceFolders, secondPositiveFilePath), 'file is within the workspace')
47+
assert.ok(!isInWorkspace(workspaceFolders, negativeFilePath), 'file is not within the workspace')
48+
})
49+
50+
it('handles the case where its the workspace itself', function () {
51+
const workspaceFolders = ['/foo']
52+
53+
assert.ok(isInWorkspace(workspaceFolders, '/foo'), 'workspace is inside itself')
54+
})
55+
})
56+
1457
describe('readDirectoryRecursively', function () {
1558
let tempFolder: TestFolder
1659
let testFeatures: TestFeatures

core/aws-lsp-core/src/util/workspaceUtils.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,18 @@ export function getEntryPath(entry: Dirent) {
8585
}
8686

8787
// TODO: port this to runtimes?
88-
export function getWorkspaceFolders(lsp: Features['lsp']): string[] {
88+
export function getWorkspaceFolderPaths(lsp: Features['lsp']): string[] {
8989
return lsp.getClientInitializeParams()?.workspaceFolders?.map(({ uri }) => URI.parse(uri).fsPath) ?? []
9090
}
9191

92-
export async function inWorkspace(workspace: Features['workspace'], filepath: string) {
93-
return (await workspace.getTextDocument(URI.file(filepath).toString())) !== undefined
92+
export function isParentFolder(parentPath: string, childPath: string): boolean {
93+
const normalizedParentPath = path.normalize(parentPath)
94+
const normalizedChildPath = path.normalize(childPath)
95+
96+
const relative = path.relative(normalizedParentPath, normalizedChildPath)
97+
return relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative)
98+
}
99+
100+
export function isInWorkspace(workspaceFolderPaths: string[], filepath: string) {
101+
return workspaceFolderPaths.some(wsFolder => isParentFolder(wsFolder, filepath) || wsFolder === filepath)
94102
}

server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/agenticChatTriggerContext.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export class AgenticChatTriggerContext {
6666
additionalContent?: AdditionalContentEntryAddition[]
6767
): GenerateAssistantResponseCommandInput {
6868
const { prompt } = params
69-
const defaultEditorState = { workspaceFolders: workspaceUtils.getWorkspaceFolders(this.#lsp) }
69+
const defaultEditorState = { workspaceFolders: workspaceUtils.getWorkspaceFolderPaths(this.#lsp) }
7070
const data: GenerateAssistantResponseCommandInput = {
7171
conversationState: {
7272
chatTriggerType: chatTriggerType,

server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/executeBash.test.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,18 @@ import { ExecuteBash } from './executeBash'
55
import { Features } from '@aws/language-server-runtimes/server-interface/server'
66
import { TestFeatures } from '@aws/language-server-runtimes/testing'
77
import { TextDocument } from 'vscode-languageserver-textdocument'
8+
import { URI } from 'vscode-uri'
9+
import { InitializeParams } from '@aws/language-server-runtimes/protocol'
810

911
describe('ExecuteBash Tool', () => {
10-
let features: Features
12+
let features: TestFeatures
13+
const workspaceFolder = '/workspace/folder'
1114

1215
before(function () {
1316
features = new TestFeatures()
17+
features.lsp.getClientInitializeParams.returns({
18+
workspaceFolders: [{ uri: URI.file(workspaceFolder).toString(), name: 'test' }],
19+
} as InitializeParams)
1420
})
1521

1622
beforeEach(() => {
@@ -76,7 +82,7 @@ describe('ExecuteBash Tool', () => {
7682
})
7783
const result = await execBash.requiresAcceptance({
7884
command: 'cat /not/in/workspace/file.txt',
79-
cwd: '/workspace/folder',
85+
cwd: workspaceFolder,
8086
})
8187

8288
assert.equal(
@@ -95,7 +101,7 @@ describe('ExecuteBash Tool', () => {
95101
getTextDocument: async s => ({}) as TextDocument,
96102
},
97103
})
98-
const result = await execBash.requiresAcceptance({ command: 'cat ./file.txt', cwd: '/workspace/folder' })
104+
const result = await execBash.requiresAcceptance({ command: 'cat ./file.txt', cwd: workspaceFolder })
99105

100106
assert.equal(result.requiresAcceptance, false, 'Relative path inside workspace should not require acceptance')
101107
})
@@ -108,7 +114,7 @@ describe('ExecuteBash Tool', () => {
108114
getTextDocument: async s => ({}) as TextDocument,
109115
},
110116
})
111-
const result = await execBash.requiresAcceptance({ command: 'echo hello world', cwd: '/workspace/folder' })
117+
const result = await execBash.requiresAcceptance({ command: 'echo hello world', cwd: workspaceFolder })
112118

113119
assert.equal(
114120
result.requiresAcceptance,

server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/executeBash.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ChildProcess, ChildProcessOptions } from '@aws/lsp-core/out/util/proces
99
// eslint-disable-next-line import/no-nodejs-modules
1010
import { isAbsolute, join } from 'path' // Safe to import on web since this is part of path-browserify
1111
import { Features } from '../../types'
12+
import { getWorkspaceFolderPaths } from '@aws/lsp-core/out/util/workspaceUtils'
1213

1314
export enum CommandCategory {
1415
ReadOnly,
@@ -122,11 +123,11 @@ interface TimestampedChunk {
122123

123124
export class ExecuteBash {
124125
private childProcess?: ChildProcess
125-
private readonly workspace: Features['workspace']
126126
private readonly logging: Features['logging']
127-
constructor(features: Pick<Features, 'logging' | 'workspace'> & Partial<Features>) {
128-
this.workspace = features.workspace
127+
private readonly lsp: Features['lsp']
128+
constructor(features: Pick<Features, 'logging' | 'lsp'> & Partial<Features>) {
129129
this.logging = features.logging
130+
this.lsp = features.lsp
130131
}
131132

132133
public async validate(command: string): Promise<void> {
@@ -195,7 +196,7 @@ export class ExecuteBash {
195196
if (this.looksLikePath(arg)) {
196197
// If not absolute, resolve using workingDirectory if available.
197198
const fullPath = !isAbsolute(arg) && params.cwd ? join(params.cwd, arg) : arg
198-
const isInWorkspace = await workspaceUtils.inWorkspace(this.workspace, fullPath)
199+
const isInWorkspace = workspaceUtils.isInWorkspace(getWorkspaceFolderPaths(this.lsp), fullPath)
199200
if (!isInWorkspace) {
200201
return { requiresAcceptance: true, warning: destructiveCommandWarningMessage }
201202
}

server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/listDirectory.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { workspaceUtils } from '@aws/lsp-core'
44
import { Features } from '@aws/language-server-runtimes/server-interface/server'
55
import { sanitize } from '@aws/lsp-core/out/util/path'
66
import { DEFAULT_EXCLUDE_PATTERNS } from '../../chat/constants'
7+
import { getWorkspaceFolderPaths } from '@aws/lsp-core/out/util/workspaceUtils'
78

89
export interface ListDirectoryParams {
910
path: string
@@ -13,10 +14,12 @@ export interface ListDirectoryParams {
1314
export class ListDirectory {
1415
private readonly logging: Features['logging']
1516
private readonly workspace: Features['workspace']
17+
private readonly lsp: Features['lsp']
1618

17-
constructor(features: Pick<Features, 'logging' | 'workspace'>) {
19+
constructor(features: Pick<Features, 'logging' | 'workspace' | 'lsp'>) {
1820
this.logging = features.logging
1921
this.workspace = features.workspace
22+
this.lsp = features.lsp
2023
}
2124

2225
public async validate(params: ListDirectoryParams): Promise<void> {
@@ -49,7 +52,7 @@ export class ListDirectory {
4952
}
5053

5154
public async requiresAcceptance(path: string): Promise<CommandValidation> {
52-
return { requiresAcceptance: !(await workspaceUtils.inWorkspace(this.workspace, path)) }
55+
return { requiresAcceptance: !workspaceUtils.isInWorkspace(getWorkspaceFolderPaths(this.lsp), path) }
5356
}
5457

5558
public async invoke(params: ListDirectoryParams): Promise<InvokeOutput> {

server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/toolServer.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import { LspGetDocuments, LspGetDocumentsParams } from './lspGetDocuments'
77
import { LspReadDocumentContents, LspReadDocumentContentsParams } from './lspReadDocumentContents'
88
import { LspApplyWorkspaceEdit, LspApplyWorkspaceEditParams } from './lspApplyWorkspaceEdit'
99

10-
export const FsToolsServer: Server = ({ workspace, logging, agent }) => {
10+
export const FsToolsServer: Server = ({ workspace, logging, agent, lsp }) => {
1111
const fsReadTool = new FsRead({ workspace, logging })
1212
const fsWriteTool = new FsWrite({ workspace, logging })
1313

14-
const listDirectoryTool = new ListDirectory({ workspace, logging })
14+
const listDirectoryTool = new ListDirectory({ workspace, logging, lsp })
1515

1616
agent.addTool(fsReadTool.getSpec(), async (input: FsReadParams) => {
1717
// TODO: fill in logic for handling invalid tool invocations
@@ -32,8 +32,8 @@ export const FsToolsServer: Server = ({ workspace, logging, agent }) => {
3232
return () => {}
3333
}
3434

35-
export const BashToolsServer: Server = ({ logging, workspace, agent }) => {
36-
const bashTool = new ExecuteBash({ logging, workspace })
35+
export const BashToolsServer: Server = ({ logging, workspace, agent, lsp }) => {
36+
const bashTool = new ExecuteBash({ logging, workspace, lsp })
3737
agent.addTool(bashTool.getSpec(), (input: ExecuteBashParams) => bashTool.invoke(input))
3838
return () => {}
3939
}

0 commit comments

Comments
 (0)