Skip to content

Commit 2d4ac99

Browse files
committed
feat: add welcome page
1 parent afc5510 commit 2d4ac99

File tree

20 files changed

+289
-63
lines changed

20 files changed

+289
-63
lines changed

.eslintrc

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
"project": "./tsconfig.json"
66
},
77
"rules": {
8-
"no-console": ["warn", { "allow": ["debug", "error", "info", "warn"] }],
9-
108
"import/no-extraneous-dependencies": "off",
119

1210
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]

__mocks__/vscode.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
// import type { WorkspaceFolder } from 'vscode'
22

3+
export const env = {
4+
openExternal: jest.fn(),
5+
}
6+
7+
export const Uri = {
8+
parse: jest.fn(value => value),
9+
}
10+
311
export const workspace = {
412
openTextDocument: jest.fn().mockResolvedValue({
513
getText: jest.fn().mockReturnValue('Document source.'),
@@ -16,5 +24,8 @@ export const workspace = {
1624
}
1725

1826
export const window = {
27+
createOutputChannel: jest.fn(() => ({
28+
appendLine: jest.fn(),
29+
})),
1930
showErrorMessage: jest.fn(),
2031
}

docs/WELCOME.md

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@
88
- [Chrome](https://chrome.google.com/webstore/detail/openai-forge/nnppeeohaoogddcihgdpcolkmibbkked)
99
- [Firefox](https://addons.mozilla.org/en-US/firefox/addon/openai-forge/)
1010

11-
### 2. Open a workspace (if not already done)
12-
13-
### 3. Configure the Evaluator
11+
### 2. Configure the Evaluator
1412

1513
What the heck is an **Evaluator**?
1614

@@ -21,9 +19,27 @@ OpenAI Forge use this command to detect errors in your code and send them to Cha
2119
This command is called:
2220
`OpenAI Forge: Evaluate errors and send them with the current document or stack` (<kbd>SHIFT + F, E</kbd>).
2321

24-
### 4. Open a [ChatGPT](https://chat.openai.com) chat in your browser
22+
***Example:***
23+
24+
> **⚠️ IMPORTANT: You need to split ALL the command subcommand and arguments in `commandArgs`.**
25+
26+
`.vscode/settings.json`
2527

26-
### 5. Add a few documents to your stack (or not)
28+
```json
29+
{
30+
'openai-forge.customEvaluators': [
31+
{
32+
command: 'npm',
33+
commandArgs: ['run', 'build'],
34+
extensions: ['.js', '.jsx', '.ts', '.tsx'],
35+
},
36+
],
37+
}
38+
```
39+
40+
### 3. Open a [ChatGPT](https://chat.openai.com) chat in your browser
41+
42+
### 4. Add a few documents to your stack (or not)
2743

2844
Run the command `OpenAI Forge: Add/Remove current document to/from the stack` (<kbd>SHIFT + F, A</kbd>)
2945
to select/unselect your currently edited document.
@@ -35,18 +51,22 @@ You can see them in the status bar:
3551

3652
Or don't, if you just want to send your currently edited document alone.
3753

38-
### 6. Send them to ChatGPT
54+
### 5. Send them to ChatGPT
3955

4056
Run the command `OpenAI Forge: Send current document or stack` (<kbd>SHIFT + F, S</kbd>).
4157

42-
### 7. Useful Key Bindings
58+
### 6. Useful Key Bindings
4359

4460
All OpenAI Forge default key bindings start with <kbd>SHIFT + F</kbd> (**F** for **F**orge):
4561

4662
- <kbd>SHIFT + F, A</kbd>: `OpenAI Forge: Add/Remove current document to/from the stack`
4763
- <kbd>SHIFT + F, E</kbd>: `OpenAI Forge: Evaluate errors and send them with the current document or stack`
4864
- <kbd>SHIFT + F, S</kbd>: `OpenAI Forge: Send current document or stack`
4965

50-
### 8. Star [my repo](https://github.com/ivangabriele/openai-forge-vsce) if you like it 🥰
66+
### 7. Star [my repo](https://github.com/ivangabriele/openai-forge-vsce) if you like it 🥰
67+
68+
### 8. May the Forge be with you 🔨
5169

52-
### 9. May the Forge be with you 🔨!
70+
- Post your ideas and questions [there](https://github.com/ivangabriele/openai-forge-vsce/discussions).
71+
- Post your issues [there](https://github.com/ivangabriele/openai-forge-vsce/issues).
72+
- If there is an internal error, you should be able to find it in **Output > OpenAI Forge**.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"rollup-plugin-swc3": "0.8.2",
5959
"semantic-release": "21.0.7",
6060
"tslib": "2.6.0",
61+
"type-fest": "4.0.0",
6162
"typescript": "5.1.6",
6263
"ws": "8.13.0"
6364
},
@@ -119,7 +120,7 @@
119120
"description": "Source code file extensions triggering this evaluator before being sent to ChatGPT. Examples: `[\".rs\"]`, `[\".js\", \".ts\"]`.",
120121
"items": {
121122
"type": "string",
122-
"pattern": "^\\.[a-z]$"
123+
"pattern": "^\\.[a-z]+$"
123124
},
124125
"minItems": 1
125126
},

src/commands/welcome.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { join } from 'path'
2+
import { Position, Range, ViewColumn, WorkspaceEdit, window, workspace } from 'vscode'
3+
4+
import { DocumentationPath } from '../constants'
5+
import { handleMessageItems } from '../helpers/handleMessageItems'
6+
import { isEmpty } from '../helpers/isEmpty'
7+
import { GlobalStateKey, getGlobalStateManager } from '../libs/GlobalStateManager'
8+
import { MessageItemType, type MessageButton } from '../types'
9+
import { getUserWorkspaceRootPath } from '../utils/getUserWorkspaceRootPath'
10+
import { showDocumentation } from '../utils/showDocumentation'
11+
12+
const HIDE_MESSAGE_BUTTON: MessageButton = {
13+
action: async () => {
14+
await getGlobalStateManager().set(GlobalStateKey.ONBOARDING__HIDE_WELCOME_PAGE, true)
15+
},
16+
label: 'Never show welcome page again',
17+
type: MessageItemType.BUTTON,
18+
}
19+
20+
export async function welcome() {
21+
const isWelcomePageHidden = await getGlobalStateManager().get(GlobalStateKey.ONBOARDING__HIDE_WELCOME_PAGE)
22+
if (isWelcomePageHidden) {
23+
return
24+
}
25+
26+
await showDocumentation(DocumentationPath.WELCOME)
27+
28+
const workspaceSettingsPath = join(getUserWorkspaceRootPath(), '.vscode', 'settings.json')
29+
const workspaceSettingsDocument = await workspace.openTextDocument(workspaceSettingsPath)
30+
const workspaceSettingsText = workspaceSettingsDocument.getText()
31+
32+
if (!workspaceSettingsText.includes('openai-forge.customEvaluators')) {
33+
const workspaceSettings = !isEmpty(workspaceSettingsText) ? JSON.parse(workspaceSettingsText) : {}
34+
const suggestedWorkspaceSettings = JSON.stringify(
35+
{
36+
...workspaceSettings,
37+
'openai-forge.customEvaluators': [
38+
{
39+
command: 'npm',
40+
commandArgs: ['run', 'build'],
41+
extensions: ['.js', '.jsx', '.ts', '.tsx'],
42+
},
43+
],
44+
},
45+
// eslint-disable-next-line no-null/no-null
46+
null,
47+
2,
48+
)
49+
const workspaceSettingsEdit = new WorkspaceEdit()
50+
workspaceSettingsEdit.replace(
51+
workspaceSettingsDocument.uri,
52+
new Range(
53+
new Position(0, 0),
54+
workspaceSettingsDocument.lineAt(workspaceSettingsDocument.lineCount - 1).range.end,
55+
),
56+
suggestedWorkspaceSettings,
57+
)
58+
59+
await window.showTextDocument(workspaceSettingsDocument, ViewColumn.Two)
60+
await workspace.applyEdit(workspaceSettingsEdit)
61+
62+
window
63+
.showInformationMessage("Once you've read the welcome page:", HIDE_MESSAGE_BUTTON.label)
64+
.then(handleMessageItems([HIDE_MESSAGE_BUTTON]))
65+
}
66+
}

src/extension.ts

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
1-
import { join } from 'path'
2-
import { type ExtensionContext, workspace, commands, window, ViewColumn } from 'vscode'
1+
import { type ExtensionContext, workspace, commands, ExtensionMode } from 'vscode'
32

43
import { addOrRemoveCurrentDocument } from './commands/addOrRemoveCurrentDocument'
54
import { evaluateAndSendCurrentDocumentOrStack } from './commands/evaluateAndSendCurrentDocumentOrStack'
65
import { sendCurrentDocument } from './commands/sendCurrentDocumentOrStack'
7-
import { DocumentationPath } from './constants'
6+
import { welcome } from './commands/welcome'
87
import { handleError } from './helpers/handleError'
9-
import { initializeGlobalStateManager } from './libs/GlobalStateManager'
8+
import { getGlobalStateManager, initializeGlobalStateManager } from './libs/GlobalStateManager'
109
import { server } from './libs/server'
1110
import { stackManager } from './libs/stackManager'
1211
import { stateManager } from './libs/stateManager'
13-
import { getUserWorkspaceRootPath } from './utils/getUserWorkspaceRootPath'
14-
import { showDocumentation } from './utils/showDocumentation'
1512

1613
export async function activate(context: ExtensionContext) {
1714
try {
@@ -26,15 +23,9 @@ export async function activate(context: ExtensionContext) {
2623
// Global State
2724

2825
await initializeGlobalStateManager(context)
29-
30-
// -------------------------------------------------------------------------
31-
// Welcome Documentation
32-
33-
await showDocumentation(DocumentationPath.WELCOME)
34-
35-
const workspaceSettingsPath = join(getUserWorkspaceRootPath(), '.vscode', 'settings.json')
36-
const settingsDocument = await workspace.openTextDocument(workspaceSettingsPath)
37-
await window.showTextDocument(settingsDocument, ViewColumn.Two)
26+
if (context.extensionMode === ExtensionMode.Development) {
27+
await getGlobalStateManager().clear()
28+
}
3829

3930
// -------------------------------------------------------------------------
4031
// Commands
@@ -67,6 +58,11 @@ export async function activate(context: ExtensionContext) {
6758
// WebSocket Server
6859

6960
server.start(context)
61+
62+
// -------------------------------------------------------------------------
63+
// Welcome Documentation
64+
65+
await welcome()
7066
} catch (err) {
7167
handleError(err)
7268

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Uri, env } from 'vscode'
2+
3+
import { MessageItemType, type MessageButton, type MessageLink } from '../../types'
4+
import { handleMessageItems } from '../handleMessageItems'
5+
6+
describe('handleMessageItems', () => {
7+
const mockButtonAction = jest.fn()
8+
const mockItems: Array<MessageButton | MessageLink> = [
9+
{ action: mockButtonAction, label: 'button', type: MessageItemType.BUTTON },
10+
{ label: 'link', type: MessageItemType.LINK, url: 'https://example.org' },
11+
]
12+
13+
beforeEach(() => {
14+
jest.clearAllMocks()
15+
})
16+
17+
it('should do nothing if itemLabel is undefined', async () => {
18+
const handleItem = handleMessageItems(mockItems)
19+
await handleItem(undefined)
20+
expect(mockButtonAction).not.toHaveBeenCalled()
21+
})
22+
23+
it('should throw an error if no item matches the itemLabel', async () => {
24+
const handleItem = handleMessageItems(mockItems)
25+
await expect(handleItem('unknown label')).rejects.toThrowError()
26+
})
27+
28+
it('should execute the action if the item type is BUTTON', async () => {
29+
const handleItem = handleMessageItems(mockItems)
30+
await handleItem('button')
31+
expect(mockButtonAction).toHaveBeenCalled()
32+
})
33+
34+
it('should open an external link if the item type is LINK', async () => {
35+
const handleItem = handleMessageItems(mockItems)
36+
await handleItem('link')
37+
expect(Uri.parse).toHaveBeenCalledWith('https://example.org')
38+
expect(env.openExternal).toHaveBeenCalledWith('https://example.org')
39+
})
40+
41+
it('should throw an error if the item type is unknown', async () => {
42+
const handleItem = handleMessageItems([...mockItems, { label: 'unknown', type: 'UNKNOWN' } as any])
43+
await expect(handleItem('unknown')).rejects.toThrowError()
44+
})
45+
})
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { isEmpty } from '../isEmpty'
2+
3+
describe('isEmpty', () => {
4+
it('should return true if the value is an empty string', () => {
5+
expect(isEmpty('')).toBe(true)
6+
})
7+
8+
it('should return true if the value is a string with spaces', () => {
9+
expect(isEmpty(' ')).toBe(true)
10+
})
11+
12+
it('should return false if the value is a non-empty string', () => {
13+
expect(isEmpty('hello')).toBe(false)
14+
})
15+
})

src/helpers/handleMessageItems.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Uri, env } from 'vscode'
2+
3+
import { InternalError } from '../libs/InternalError'
4+
import { MessageItemType, type MessageButton, type MessageLink } from '../types'
5+
6+
export function handleMessageItems(items: Array<MessageButton | MessageLink>) {
7+
return async function handleMessageItem(itemLabel: string | undefined) {
8+
if (!itemLabel) {
9+
return
10+
}
11+
12+
const item = items.find(({ label }) => label === itemLabel)
13+
if (!item) {
14+
throw new InternalError(`This \`item\` does not exist: "${itemLabel}".`)
15+
}
16+
17+
switch (item.type) {
18+
case MessageItemType.BUTTON:
19+
await item.action()
20+
break
21+
22+
case MessageItemType.LINK:
23+
await env.openExternal(Uri.parse(item.url))
24+
break
25+
26+
default:
27+
throw new InternalError(`Unknown \`MessageItemType\`: "${(item as any).type}".`)
28+
}
29+
}
30+
}

src/helpers/isEmpty.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { isEmpty as ramdaIsEmpty } from 'ramda'
2+
3+
export function isEmpty(value: any): boolean {
4+
if (typeof value === 'string') {
5+
return value.trim() === ''
6+
}
7+
8+
return ramdaIsEmpty(value)
9+
}

0 commit comments

Comments
 (0)