Skip to content

Commit fb30b2f

Browse files
committed
Initial implementation of #4
1 parent c64a376 commit fb30b2f

File tree

6 files changed

+249
-11
lines changed

6 files changed

+249
-11
lines changed

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@
8585
"command": "resx-editor.deleteResource",
8686
"title": "Delete Resource",
8787
"category": "ResX Editor"
88+
},
89+
{
90+
"command": "resx-editor.addNewResource",
91+
"title": "Add New Resource",
92+
"category": "ResX Editor"
8893
}
8994
]
9095
},

src/addNewResource.ts

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import { window, Disposable, QuickInput, QuickInputButtons, ExtensionContext } from 'vscode';
2+
3+
export async function newResourceInput(context: ExtensionContext) {
4+
5+
6+
interface State {
7+
title: string;
8+
step: number;
9+
totalSteps: number;
10+
key: string;
11+
value: string;
12+
comment: string | string;
13+
}
14+
15+
async function collectInputs() {
16+
const state = {} as Partial<State>;
17+
await MultiStepInput.run(input => inputKey(input, state));
18+
return state as State;
19+
}
20+
21+
const title = 'Add new resource';
22+
23+
async function inputKey(input: MultiStepInput, state: Partial<State>) {
24+
state.key = await input.showInputBox({
25+
title,
26+
step: 1,
27+
totalSteps: 3,
28+
value: state.key || '',
29+
prompt: 'Provide a Key for the resource',
30+
validate: validateNotNull,
31+
shouldResume: shouldResume
32+
});
33+
return (input: MultiStepInput) => inputValue(input, state);
34+
}
35+
36+
async function inputValue(input: MultiStepInput, state: Partial<State>) {
37+
state.value = await input.showInputBox({
38+
title,
39+
step: 2,
40+
totalSteps: 3,
41+
value: state.value || '',
42+
prompt: 'Provide a Value for the resource',
43+
validate: validateNotNull,
44+
shouldResume: shouldResume
45+
});
46+
return (input: MultiStepInput) => inputComment(input, state);
47+
}
48+
49+
async function inputComment(input: MultiStepInput, state: Partial<State>) {
50+
state.comment = await input.showInputBox({
51+
title,
52+
step: 3,
53+
totalSteps: 3,
54+
value: state.comment || '',
55+
prompt: 'Provide a Comment for the resource',
56+
validate: validateNotNull,
57+
shouldResume: shouldResume
58+
});
59+
}
60+
61+
function shouldResume() {
62+
// Could show a notification with the option to resume.
63+
return new Promise<boolean>((resolve, reject) => {
64+
// noop
65+
});
66+
}
67+
68+
async function validateNotNull(name: string) {
69+
await new Promise(resolve => setTimeout(resolve, 1000));
70+
return name === '' ? 'Must not be empty' : undefined;
71+
}
72+
73+
const state = await collectInputs();
74+
75+
return state;
76+
}
77+
78+
79+
// -------------------------------------------------------
80+
// Helper code that wraps the API for the multi-step case.
81+
// -------------------------------------------------------
82+
83+
84+
class InputFlowAction {
85+
static back = new InputFlowAction();
86+
static cancel = new InputFlowAction();
87+
static resume = new InputFlowAction();
88+
}
89+
90+
type InputStep = (input: MultiStepInput) => Thenable<InputStep | void>;
91+
92+
interface InputBoxParameters {
93+
title: string;
94+
step: number;
95+
totalSteps: number;
96+
value: string;
97+
prompt: string;
98+
placeholder?: string;
99+
validate: (value: string) => Promise<string | undefined>;
100+
shouldResume: () => Thenable<boolean>;
101+
}
102+
103+
class MultiStepInput {
104+
105+
static async run<T>(start: InputStep) {
106+
const input = new MultiStepInput();
107+
return input.stepThrough(start);
108+
}
109+
110+
private current?: QuickInput;
111+
private steps: InputStep[] = [];
112+
113+
private async stepThrough<T>(start: InputStep) {
114+
let step: InputStep | void = start;
115+
while (step) {
116+
this.steps.push(step);
117+
if (this.current) {
118+
this.current.enabled = false;
119+
this.current.busy = true;
120+
}
121+
try {
122+
step = await step(this);
123+
} catch (err) {
124+
if (err === InputFlowAction.back) {
125+
this.steps.pop();
126+
step = this.steps.pop();
127+
} else if (err === InputFlowAction.resume) {
128+
step = this.steps.pop();
129+
} else if (err === InputFlowAction.cancel) {
130+
step = undefined;
131+
} else {
132+
throw err;
133+
}
134+
}
135+
}
136+
if (this.current) {
137+
this.current.dispose();
138+
}
139+
}
140+
141+
async showInputBox<P extends InputBoxParameters>({ title, step, totalSteps, value, prompt, validate, buttons, shouldResume, placeholder }: P) {
142+
const disposables: Disposable[] = [];
143+
try {
144+
return await new Promise<string | (P extends { buttons: (infer I)[] } ? I : never)>((resolve, reject) => {
145+
const input = window.createInputBox();
146+
input.title = title;
147+
input.step = step;
148+
input.totalSteps = totalSteps;
149+
input.value = value || '';
150+
input.prompt = prompt;
151+
input.buttons = [
152+
...(this.steps.length > 1 ? [QuickInputButtons.Back] : []),
153+
...(buttons || [])
154+
];
155+
input.placeholder = placeholder;
156+
let validating = validate('');
157+
disposables.push(
158+
input.onDidTriggerButton(item => {
159+
if (item === QuickInputButtons.Back) {
160+
reject(InputFlowAction.back);
161+
} else {
162+
resolve(<any>item);
163+
}
164+
}),
165+
input.onDidAccept(async () => {
166+
const value = input.value;
167+
input.enabled = false;
168+
input.busy = true;
169+
if (!(await validate(value))) {
170+
resolve(value);
171+
}
172+
input.enabled = true;
173+
input.busy = false;
174+
}),
175+
input.onDidChangeValue(async text => {
176+
const current = validate(text);
177+
validating = current;
178+
const validationMessage = await current;
179+
if (current === validating) {
180+
input.validationMessage = validationMessage;
181+
}
182+
}),
183+
input.onDidHide(() => {
184+
(async () => {
185+
reject(shouldResume && await shouldResume() ? InputFlowAction.resume : InputFlowAction.cancel);
186+
})()
187+
.catch(reject);
188+
})
189+
);
190+
if (this.current) {
191+
this.current.dispose();
192+
}
193+
this.current = input;
194+
this.current.show();
195+
});
196+
} finally {
197+
disposables.forEach(d => d.dispose());
198+
}
199+
}
200+
}

src/commands.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import * as vscode from 'vscode';
2+
import { window, ExtensionContext } from 'vscode';
3+
import { newResourceInput } from './addNewResource';
4+
5+
export async function addNewResourceHandler (context: ExtensionContext) {
6+
// get all the inputs we need
7+
const inputs = newResourceInput(context);
8+
};

src/extension.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,8 @@ export const printChannelOutput = (content: string, verbose: boolean, reveal = f
3232
}
3333

3434
const timestamp = new Date().toISOString();
35-
const fileName = vscode.window.activeTextEditor?.document.fileName;
3635

37-
outputChannel.appendLine(`[${timestamp}] [${fileName}] ${content}`);
36+
outputChannel.appendLine(`[${timestamp}] ${content}`);
3837

3938
if (reveal) {
4039
outputChannel.show(true);

src/resxProvider.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as vscode from 'vscode';
33
import resx from 'resx';
44
import { getNonce } from './utilities/getNonce';
55
import { printChannelOutput } from './extension';
6+
import { newResourceInput } from './addNewResource';
67

78
export class ResxProvider implements vscode.CustomTextEditorProvider {
89

@@ -33,19 +34,34 @@ export class ResxProvider implements vscode.CustomTextEditorProvider {
3334
};
3435
webviewPanel.webview.html = this._getWebviewContent(webviewPanel.webview);
3536

36-
try
37-
{
38-
if (!this.registered) {
39-
printChannelOutput("deleteResource command registered", true);
40-
this.registered = true;
41-
let disposable = vscode.commands.registerCommand('resx-editor.deleteResource', () => {
37+
try {
38+
if (!this.registered) {
39+
printChannelOutput("deleteResource command registered", true);
40+
this.registered = true;
41+
let deleteCommand = vscode.commands.registerCommand('resx-editor.deleteResource', () => {
4242

43-
this.currentPanel?.webview.postMessage({
44-
type: 'delete'
43+
this.currentPanel?.webview.postMessage({
44+
type: 'delete'
45+
});
4546
});
47+
48+
let addCommand = vscode.commands.registerCommand('resx-editor.addNewResource', () => {
49+
// get all the inputs we need
50+
const inputs = newResourceInput(this.context);
51+
// then do something with them
52+
inputs.then((result) => {
53+
printChannelOutput(`Adding new resource: Key: ${result.key}, Value: ${result.value}, Comment: ${result.comment}`, true);
54+
this.currentPanel?.webview.postMessage({
55+
type: 'add',
56+
key: result.key,
57+
value: result.value,
58+
comment: result.comment
59+
});
60+
});
4661
});
4762

48-
this.context.subscriptions.push(disposable);
63+
this.context.subscriptions.push(deleteCommand);
64+
this.context.subscriptions.push(addCommand);
4965
}
5066
}
5167
catch (e)

src/webview/webview.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,16 @@ let currentRowData = null;
8181
}
8282
}
8383
return;
84+
case 'add':
85+
sendLog(`Adding new resource: Key: ${message.key}, Value: ${message.value}, Comment: ${message.comment}`);
86+
if (message.key) {
87+
const index = table.rowsData.findIndex(x => x.Key === message.key);
88+
if (index === -1) {
89+
table.rowsData.push({ Key: message.key, Value: message.value, Comment: message.comment });
90+
refreshResxData();
91+
}
92+
}
93+
return;
8494
}
8595
});
8696

0 commit comments

Comments
 (0)