diff --git a/README.md b/README.md index ef81dc4..95bbb0b 100644 --- a/README.md +++ b/README.md @@ -121,5 +121,11 @@ Development Host](https://code.visualstudio.com/api/advanced-topics/extension-ho --- +If you get an error about a `NODE_MODULE_VERSION` incompatibility or that a file is not a Windows executable, +this is likely the error described above. Run `npm i -g electron-rebuild` if you have not done so and follow the +directions above. + +--- + See the Visual Studio Code [getting started](https://code.visualstudio.com/api/get-started/your-first-extension) API page if you need more help. diff --git a/src/accessNodeProvider.ts b/src/accessNodeProvider.ts deleted file mode 100644 index 9b0db83..0000000 --- a/src/accessNodeProvider.ts +++ /dev/null @@ -1,47 +0,0 @@ -import * as vscode from 'vscode'; - -// list of all actions -let actions: AccessAction[] = []; - -class AccessAction extends vscode.TreeItem { - constructor( - public readonly label: string, - public readonly command: vscode.Command - ) { - super(label, vscode.TreeItemCollapsibleState.None); - } -}; - -export default class AccessNodeProvider implements vscode.TreeDataProvider { - public getTreeItem(a: AccessAction): vscode.TreeItem { - return a; - } - - public async getChildren(): Promise { - if (actions.length === 0) { - // fetch and cache mind-reader options - let cmds: string[] = await vscode.commands.getCommands(true); // get non-builtin commands - cmds = cmds.filter(x => x.startsWith('mind-reader')); // filter mind-reader commands - - cmds.forEach(c => { - let humanReadable = c.replace(/^mind-reader\./, ''); // strip extensions name - - // Convert camelCaseText to Title Case Text - humanReadable = humanReadable.replace(/([A-Z])/g, ' $1'); - humanReadable = humanReadable.charAt(0).toUpperCase() + humanReadable.slice(1); - - // add item to actions - actions.push(new AccessAction( - humanReadable, - { - title: humanReadable, - command: c, - tooltip: humanReadable - } - )); - }); - } - - return Promise.resolve(actions); - } -} diff --git a/src/commands.ts b/src/commands.ts deleted file mode 100644 index 2970d2a..0000000 --- a/src/commands.ts +++ /dev/null @@ -1,522 +0,0 @@ -import * as vscode from 'vscode'; -import * as pl from './pylex'; -import * as path from 'path'; -import * as fs from 'fs'; - -import HubManager from './hubManager'; - -/** - * @type {Object} Command // Command to register with the VS Code Extension API - * @prop {string} command // Name of the command; e.g., 'mind-reader.selectTheme' - * @prop {callback} callback // Callback to register when `command` is invoked - */ -export type CommandEntry = { - name: string, - callback: () => void -}; - -// Accessibility Commands -export const accessCommands: CommandEntry[] = [ - { - name: 'mind-reader.selectTheme', - - // callbacks can be inlined... - callback: () => vscode.commands.executeCommand('workbench.action.selectTheme'), - }, - - { - name: 'mind-reader.increaseFontScale', - callback: increaseFontScale, // ...or factored out into separate functions below - }, - - { - name: 'mind-reader.decreaseFontScale', - callback: decreaseFontScale, - }, - - { - name: 'mind-reader.resetFontScale', - callback: resetFontScale, - }, - - { - name: 'mind-reader.increaseEditorScale', - callback: increaseEditorScale, - }, - - { - name: 'mind-reader.decreaseEditorScale', - callback: decreaseEditorScale, - }, - - { - name: 'mind-reader.resetEditorScale', - callback: resetEditorScale, - }, - - { - name: 'mind-reader.getIndent', - callback: getIndent, - }, - - { - name: 'mind-reader.runLineContext', - callback: runLineContext, - }, - - { - name: 'mind-reader.runCursorContext', - callback: runCursorContext - } -]; - -export const navCommands: CommandEntry[] = [ - { - name: 'mind-reader.openWebview', - callback: openWebview, - }, - - { - name: 'mind-reader.openKeyBindWin', - callback: () => openKeyBindWin('Windows') - }, - { - name: 'mind-reader.openKeyBindMac', - callback: () => openKeyBindWin('Mac'), - }, - - //Navigation Keys...... - { - name: 'mind-reader.showAllSymbols', - callback: () => vscode.commands.executeCommand('workbench.action.showAllSymbols'), - }, - - { - name: 'mind-reader.gotoLine', - callback: () => vscode.commands.executeCommand('workbench.action.gotoLine'), - }, - - { - name: 'mind-reader.quickOpen', - callback: () => vscode.commands.executeCommand('workbench.action.quickOpen'), - }, - - { - name: 'mind-reader.gotoSymbol', - callback: () => vscode.commands.executeCommand('workbench.action.gotoSymbol'), - }, - - { - name: 'mind-reader.showProblems', - callback: () => vscode.commands.executeCommand('workbench.actions.view.problems'), - }, - - { - name: 'mind-reader.nextInFiles', - callback: () => vscode.commands.executeCommand('editor.action.marker.nextInFiles'), - }, - - { - name: 'mind-reader.prevInFiles', - callback: () => vscode.commands.executeCommand('editor.action.marker.prevInFiles'), - }, - - { - name: 'mind-reader.showCommands', - callback: () => vscode.commands.executeCommand('workbench.action.showCommands'), - }, - - { - name: 'mind-reader.quickOpenPreviousRecentlyUsedEditorInGroup', - callback: () => vscode.commands.executeCommand('workbench.action.quickOpenPreviousRecentlyUsedEditorInGroup'), - }, - - { - name: 'mind-reader.navigateBack', - callback: () => vscode.commands.executeCommand('workbench.action.navigateBack'), - }, - - { - name: 'mind-reader.getQuickInputBack', - callback: () => vscode.commands.executeCommand('workbench.action.quickInputBack'), - }, - - { - name: 'mind-reader.navigateForward', - callback: () => vscode.commands.executeCommand('workbench.action.navigateForward'), - }, -]; - -export const hubCommands: CommandEntry[] = [ - { - name: 'mind-reader.connectHub', - callback: connectHub - }, - - { - name: 'mind-reader.disconnectHub', - callback: disconnectHub - }, - - { - name: 'mind-reader.uploadCurrentFile', - callback: uploadCurrentFile - }, - - { - name: 'mind-reader.runProgram', - callback: runProgram - }, - - { - name: 'mind-reader.stopExecution', - callback: stopExecution - }, - - { - name: 'mind-reader.deleteProgram', - callback: deleteProgram - }, -]; - -// COMMAND CALLBACK IMPLEMENTATIONS - -function increaseFontScale(): void { - vscode.commands.executeCommand('editor.action.fontZoomIn'); -} - -function decreaseFontScale(): void { - vscode.commands.executeCommand('editor.action.fontZoomOut'); -} - -function resetFontScale(): void { - vscode.commands.executeCommand('editor.action.fontZoomReset'); -} - -function increaseEditorScale(): void { - vscode.commands.executeCommand('workbench.action.zoomIn'); -} - -function decreaseEditorScale(): void { - vscode.commands.executeCommand('workbench.action.zoomOut'); -} - -function resetEditorScale(): void { - vscode.commands.executeCommand('workbench.action.zoomReset'); -} - -function openWebview(): void { - //vscode.commands.executeCommand('workbench.action.zoomOut'); - const panel = vscode.window.createWebviewPanel( - 'mindReader', // Identifies the type of the webview. Used internally - 'Mind Reader', // Title of the panel displayed to the user - vscode.ViewColumn.One, // Editor column to show the new webview panel in. - {} - ); // Webview options. More on these later. - - panel.webview.html = getWebviewContent('media/html/main.html'); -} - -function getWebviewContent(filepath: string) { - return fs.readFileSync(filepath, {encoding: 'utf-8'}); -} - -function openKeyBindWin(os: 'Mac' | 'Windows'): void { - //vscode.commands.executeCommand('workbench.action.zoomOut'); - const panel = vscode.window.createWebviewPanel( - 'mindReader', // Identifies the type of the webview. Used internally - 'MR Key Bindings', // Title of the panel displayed to the user - vscode.ViewColumn.One, // Editor column to show the new webview panel in. - {} - ); // Webview options. More on these later. - - if (os === 'Windows') { - panel.webview.html = getWebviewContent('media/html/winkeys.html'); - } else if (os === 'Mac') { - panel.webview.html = getWebviewContent('media/html/mackeys.html'); - } -} - -function getIndent(): void { - let editor = vscode.window.activeTextEditor; - if(editor) - { - let lineNum = editor.selection.active.line + 1; - let textLine = editor.document.lineAt(lineNum - 1); - if(textLine.isEmptyOrWhitespace) - { - vscode.window.showInformationMessage("Line number " + lineNum.toString() + " Is Empty"); - } - else - { - // Grab tab format from open document - let tabFmt = { - size: editor.options.tabSize as number, - hard: !editor.options.insertSpaces - }; - let i = pl.Lexer.getIndent(textLine.text, tabFmt); - vscode.window.showInformationMessage("Line Number " + lineNum.toString() + " Indentation " + i.toString()); - } - } - else{ - vscode.window.showErrorMessage('No document currently active'); - } - -} - -function runLineContext(): void { - let editor = vscode.window.activeTextEditor; - if (editor){ - // current text and line number - let editorText = editor.document.getText(); - let line = editor.selection.active.line; - - // get tab info settings - let size = parseInt(editor.options.tabSize as string); - let hard = !editor.options.insertSpaces; - - // initialize parser - let parser = new pl.Parser(editorText, {size, hard}); - parser.parse(); - - let context = parser.context(line); - - // build text - let contentString = createContextString(context, line); - vscode.window.showInformationMessage(contentString); - } else { - vscode.window.showErrorMessage('No document currently active'); - } -} - -function createContextString(context: pl.LexNode[], line: number): string { - if (context.length < 1) { - throw new Error('Cannot create context string for empty context'); - } - - let contextString = 'Line ' + (line+1); // 1 based - if (context[0].token && context[0].token.attr) { - contextString += ': ' + context[0].token.type.toString() + ' ' + context[0].token.attr.toString(); - } - for (let i = 1; i < context.length; i++) { - let node = context[i]; - if (node.label === 'root') { - // root - contextString += ' in the Document Root'; - continue; - } - - if (node.token!.type !== pl.PylexSymbol.EMPTY && - node.token!.type !== pl.PylexSymbol.INDENT) { - contextString += ' inside ' + node.token!.type.toString(); - if (node.token!.attr) { - contextString += ' ' + node.token!.attr.toString(); - } - } - } - return contextString; -} - -// find up to `n` words around the cursor, where `n` is -// the value of `#mindReader.reader.contextWindow` -function runCursorContext(): void { - let editor = vscode.window.activeTextEditor; - if (!editor) { - vscode.window.showErrorMessage('RunCursorContext: No Active Editor'); - return; - } - - const cursorPos: vscode.Position = editor.selection.active; - const text: string = editor.document.lineAt(cursorPos).text; - const windowSize: number = vscode.workspace.getConfiguration('mindReader').get('reader.contextWindow')!; - - let trimmedText = text.trimStart(); // trim leading whitespace - let leadingWS = text.length - trimmedText.length; // # of characters of leading whitespace - trimmedText = trimmedText.trimEnd(); // trim trailing whitespace - let pos = leadingWS; - let maxPos = text.length; - - // clamp cursor start/end to new range - let col = cursorPos.character; // effective column of the cursor position - if (col < leadingWS) { - // move effective start to first non-whitespace character in the line - col = leadingWS; - } else if (col > leadingWS + trimmedText.length - 1) { - // move effective end to last non-whitespace character in the line - col = leadingWS + trimmedText.length - 1; - } - - // generate list of space separate words with range data (start, end) - // TODO: can find user position to be done in one pass - let spaceWords: {word: string, start: number, end: number}[] = []; - while (pos < maxPos && trimmedText.length > 0) { - let word = trimmedText.replace(/ .*/, ''); - spaceWords.push({word, start: pos, end: pos+word.length}); - - // remove processed word from trimmed text - const oldText = trimmedText; - trimmedText = trimmedText.replace(/[^ ]+/, '').trimStart(); - - // update pos to start of next word - pos += oldText.length - trimmedText.length; - } - - // find word the user is in - let contextStart: number = -1, contextEnd: number = -1; - for (let i = 0; i < spaceWords.length; i++) { - if (col >= spaceWords[i].start && col <= spaceWords[i].end) { - // found the word - contextStart = Math.max(0, i - windowSize); // clamp start index - contextEnd = Math.min(spaceWords.length, i + windowSize + 1); // clamp end index - - // construct cursor context string - let contextString = ''; - for (let i = contextStart; i < contextEnd; i++) { - contextString += spaceWords[i].word + ' '; - } - - // output cursor context string - vscode.window.showInformationMessage(contextString); - - return; - } - } -} - -// Current connected hub -let hub: HubManager | null = null; - -// TODO: port option -async function connectHub(): Promise { - if (hub && hub.isOpen()) { - vscode.window.showWarningMessage('LEGO Hub is already connected, reconnecting...'); - disconnectHub(); - } - - try { - const ports = await HubManager.queryPorts(); - - if (ports.length === 0) { - vscode.window.showErrorMessage('No ports found. Is the LEGO Hub connected?'); - return; - } - - let portPath: string | undefined = vscode.workspace.getConfiguration('mindReader.connection').get('portPath'); - - if (!portPath) { - let slots: vscode.QuickPickItem[] = []; - for (const port of ports) { - slots.push({ label: port.path }); - } - - let picked = await vscode.window.showQuickPick(slots); - - if (!picked) { - return; - } - - portPath = picked.label; - } - hub = await HubManager.create(portPath); - vscode.window.showInformationMessage('LEGO Hub connected'); - } catch (err) { - vscode.window.showErrorMessage('Could not connect to LEGO Hub'); - } -} - -async function disconnectHub(): Promise { - if (!hub || !hub.isOpen()) { - vscode.window.showErrorMessage('LEGO Hub is not connected'); - return; - } - - await hub.close(); - hub = null; - vscode.window.showInformationMessage('LEGO Hub disconnected'); -} - -async function uploadCurrentFile(): Promise { - if (!hub || !hub.isOpen()) { - vscode.window.showErrorMessage('LEGO Hub is not connected!'); - return; - } - - if (!vscode.window.activeTextEditor) { - vscode.window.showErrorMessage('No active text editor'); - return; - } - - const currentFilePath = vscode.window.activeTextEditor.document.fileName; - - if (currentFilePath) { - // construct quickpick - const slots: vscode.QuickPickItem[] = []; - for (let i = 0; i < 10; i++) { - slots.push({ label: i.toString() }); - } - const slotID = await vscode.window.showQuickPick(slots, { canPickMany: false }); - - if (!slotID) { - return; - } - - // TODO: progress bar? - vscode.window.showInformationMessage('Uploading current file'); - await hub.uploadFile(currentFilePath, parseInt(slotID.label), path.basename(currentFilePath)); - vscode.window.showInformationMessage(path.basename(currentFilePath) + ' uploaded to slot ' + slotID.label); - } -} - -// TODO: find empty slots -async function runProgram(): Promise { - if (!hub || !hub.isOpen()) { - vscode.window.showErrorMessage('LEGO Hub is not connected!'); - return; - } - - const slots: vscode.QuickPickItem[] = []; - // construct quickpick - for (let i = 0; i < 10; i++) { - slots.push({ label: i.toString() }); - } - const slotID = await vscode.window.showQuickPick(slots, { canPickMany: false }); - - if (!slotID) { - return; - } - - vscode.window.showInformationMessage('Running program ' + slotID.label); - await hub.programExecute(parseInt(slotID.label)); -} - -async function stopExecution(): Promise { - if (!hub || !hub.isOpen()) { - vscode.window.showErrorMessage('LEGO Hub is not connected!'); - return; - } - - await hub.programTerminate(); - vscode.window.showInformationMessage('Execution stopped'); -} - -// TODO: find slots from status -async function deleteProgram(): Promise { - if (!hub || !hub.isOpen()) { - vscode.window.showErrorMessage('LEGO Hub is not connected!'); - return; - } - - const slots: vscode.QuickPickItem[] = []; - // construct quickpick - for (let i = 0; i < 10; i++) { - slots.push({ label: i.toString() }); - } - const slotID = await vscode.window.showQuickPick(slots, { canPickMany: false }); - - if (!slotID) { - return; - } - - await hub.deleteProgram(parseInt(slotID.label)); - vscode.window.showInformationMessage('Deleted program ' + slotID.label); -} diff --git a/src/commands/access.ts b/src/commands/access.ts new file mode 100755 index 0000000..8c05327 --- /dev/null +++ b/src/commands/access.ts @@ -0,0 +1,68 @@ +import * as vscode from 'vscode'; +import { CommandEntry } from './commandEntry'; + +// Accessibility Commands +export const accessCommands: CommandEntry[] = [ + { + name: 'mind-reader.selectTheme', + + // callbacks can be inlined... + callback: () => vscode.commands.executeCommand('workbench.action.selectTheme'), + }, + + { + name: 'mind-reader.increaseFontScale', + callback: increaseFontScale, // ...or factored out into separate functions below + }, + + { + name: 'mind-reader.decreaseFontScale', + callback: decreaseFontScale, + }, + + { + name: 'mind-reader.resetFontScale', + callback: resetFontScale, + }, + + { + name: 'mind-reader.increaseEditorScale', + callback: increaseEditorScale, + }, + + { + name: 'mind-reader.decreaseEditorScale', + callback: decreaseEditorScale, + }, + + { + name: 'mind-reader.resetEditorScale', + callback: resetEditorScale, + }, +]; + + +function increaseFontScale(): void { + vscode.commands.executeCommand('editor.action.fontZoomIn'); +} + +function decreaseFontScale(): void { + vscode.commands.executeCommand('editor.action.fontZoomOut'); +} + +function resetFontScale(): void { + vscode.commands.executeCommand('editor.action.fontZoomReset'); +} + +function increaseEditorScale(): void { + vscode.commands.executeCommand('workbench.action.zoomIn'); +} + +function decreaseEditorScale(): void { + vscode.commands.executeCommand('workbench.action.zoomOut'); +} + +function resetEditorScale(): void { + vscode.commands.executeCommand('workbench.action.zoomReset'); +} + diff --git a/src/commands/commandEntry.ts b/src/commands/commandEntry.ts new file mode 100755 index 0000000..8461836 --- /dev/null +++ b/src/commands/commandEntry.ts @@ -0,0 +1,10 @@ +/** + * @type {Object} Command // Command to register with the VS Code Extension API + * @prop {string} command // Name of the command; e.g., 'mind-reader.selectTheme' + * @prop {callback} callback // Callback to register when `command` is invoked + */ +export type CommandEntry = { + name: string, + callback: () => void +}; + diff --git a/src/commands/hub.ts b/src/commands/hub.ts new file mode 100755 index 0000000..1b8bd03 --- /dev/null +++ b/src/commands/hub.ts @@ -0,0 +1,174 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import HubManager from '../hubManager'; + +import { CommandEntry } from './commandEntry'; + +export const hubCommands: CommandEntry[] = [ + { + name: 'mind-reader.connectHub', + callback: connectHub + }, + + { + name: 'mind-reader.disconnectHub', + callback: disconnectHub + }, + + { + name: 'mind-reader.uploadCurrentFile', + callback: uploadCurrentFile + }, + + { + name: 'mind-reader.runProgram', + callback: runProgram + }, + + { + name: 'mind-reader.stopExecution', + callback: stopExecution + }, + + { + name: 'mind-reader.deleteProgram', + callback: deleteProgram + }, +]; + +// Current connected hub +let hub: HubManager | null = null; + +async function connectHub(): Promise { + if (hub && hub.isOpen()) { + vscode.window.showWarningMessage('LEGO Hub is already connected, reconnecting...'); + disconnectHub(); + } + + try { + const ports = await HubManager.queryPorts(); + + if (ports.length === 0) { + vscode.window.showErrorMessage('No ports found. Is the LEGO Hub connected?'); + return; + } + + let portPath: string | undefined = vscode.workspace.getConfiguration('mindReader.connection').get('portPath'); + + if (!portPath) { + let slots: vscode.QuickPickItem[] = []; + for (const port of ports) { + slots.push({ label: port.path }); + } + + let picked = await vscode.window.showQuickPick(slots); + + if (!picked) { + return; + } + + portPath = picked.label; + } + hub = await HubManager.create(portPath); + vscode.window.showInformationMessage('LEGO Hub connected'); + } catch (err) { + vscode.window.showErrorMessage('Could not connect to LEGO Hub'); + } +} + +async function disconnectHub(): Promise { + if (!hub || !hub.isOpen()) { + vscode.window.showErrorMessage('LEGO Hub is not connected'); + return; + } + + await hub.close(); + hub = null; + vscode.window.showInformationMessage('LEGO Hub disconnected'); +} + +async function uploadCurrentFile(): Promise { + if (!hub || !hub.isOpen()) { + vscode.window.showErrorMessage('LEGO Hub is not connected!'); + return; + } + + if (!vscode.window.activeTextEditor) { + vscode.window.showErrorMessage('No active text editor'); + return; + } + + const currentFilePath = vscode.window.activeTextEditor.document.fileName; + + if (currentFilePath) { + // construct quickpick + const slots: vscode.QuickPickItem[] = []; + for (let i = 0; i < 10; i++) { + slots.push({ label: i.toString() }); + } + const slotID = await vscode.window.showQuickPick(slots, { canPickMany: false }); + + if (!slotID) { + return; + } + + // TODO: progress bar? + vscode.window.showInformationMessage('Uploading current file'); + await hub.uploadFile(currentFilePath, parseInt(slotID.label), path.basename(currentFilePath)); + vscode.window.showInformationMessage(path.basename(currentFilePath) + ' uploaded to slot ' + slotID.label); + } +} + +// TODO: find empty slots +async function runProgram(): Promise { + if (!hub || !hub.isOpen()) { + vscode.window.showErrorMessage('LEGO Hub is not connected!'); + return; + } + + const slots: vscode.QuickPickItem[] = []; + // construct quickpick + for (let i = 0; i < 10; i++) { + slots.push({ label: i.toString() }); + } + const slotID = await vscode.window.showQuickPick(slots, { canPickMany: false }); + + if (!slotID) { + return; + } + + vscode.window.showInformationMessage('Running program ' + slotID.label); + await hub.programExecute(parseInt(slotID.label)); +} + +async function stopExecution(): Promise { + if (!hub || !hub.isOpen()) { + vscode.window.showErrorMessage('LEGO Hub is not connected!'); + return; + } + + await hub.programTerminate(); + vscode.window.showInformationMessage('Execution stopped'); +} + +// TODO: find slots from status +async function deleteProgram(): Promise { + if (!hub || !hub.isOpen()) { + vscode.window.showErrorMessage('LEGO Hub is not connected!'); + return; + } + + const slots: vscode.QuickPickItem[] = []; + // construct quickpick + for (let i = 0; i < 10; i++) { + slots.push({ label: i.toString() }); + } + const slotID = await vscode.window.showQuickPick(slots, { canPickMany: false }); + + if (!slotID) { + return; + } + + await hub.deleteProgram(parseInt(slotID.label)); + vscode.window.showInformationMessage('Deleted program ' + slotID.label); +} diff --git a/src/commands/index.ts b/src/commands/index.ts new file mode 100755 index 0000000..512519c --- /dev/null +++ b/src/commands/index.ts @@ -0,0 +1,5 @@ +export{ CommandEntry } from './commandEntry'; +export{ accessCommands } from './access'; +export{ textCommands } from './text'; +export{ hubCommands } from './hub'; +export{ navCommands } from './nav'; \ No newline at end of file diff --git a/src/commands/nav.ts b/src/commands/nav.ts new file mode 100755 index 0000000..f694e40 --- /dev/null +++ b/src/commands/nav.ts @@ -0,0 +1,116 @@ +import * as vscode from 'vscode'; +import * as fs from 'fs'; + +import { CommandEntry } from './commandEntry'; + +export const navCommands: CommandEntry[] = [ + { + name: 'mind-reader.openWebview', + callback: openWebview, + }, + + { + name: 'mind-reader.openKeyBindWin', + callback: () => openKeyBindWin('Windows') + }, + { + name: 'mind-reader.openKeyBindMac', + callback: () => openKeyBindWin('Mac'), + }, + + //Navigation Keys...... + { + name: 'mind-reader.showAllSymbols', + callback: () => vscode.commands.executeCommand('workbench.action.showAllSymbols'), + }, + + { + name: 'mind-reader.gotoLine', + callback: () => vscode.commands.executeCommand('workbench.action.gotoLine'), + }, + + { + name: 'mind-reader.quickOpen', + callback: () => vscode.commands.executeCommand('workbench.action.quickOpen'), + }, + + { + name: 'mind-reader.gotoSymbol', + callback: () => vscode.commands.executeCommand('workbench.action.gotoSymbol'), + }, + + { + name: 'mind-reader.showProblems', + callback: () => vscode.commands.executeCommand('workbench.actions.view.problems'), + }, + + { + name: 'mind-reader.nextInFiles', + callback: () => vscode.commands.executeCommand('editor.action.marker.nextInFiles'), + }, + + { + name: 'mind-reader.prevInFiles', + callback: () => vscode.commands.executeCommand('editor.action.marker.prevInFiles'), + }, + + { + name: 'mind-reader.showCommands', + callback: () => vscode.commands.executeCommand('workbench.action.showCommands'), + }, + + { + name: 'mind-reader.quickOpenPreviousRecentlyUsedEditorInGroup', + callback: () => vscode.commands.executeCommand('workbench.action.quickOpenPreviousRecentlyUsedEditorInGroup'), + }, + + { + name: 'mind-reader.navigateBack', + callback: () => vscode.commands.executeCommand('workbench.action.navigateBack'), + }, + + { + name: 'mind-reader.getQuickInputBack', + callback: () => vscode.commands.executeCommand('workbench.action.quickInputBack'), + }, + + { + name: 'mind-reader.navigateForward', + callback: () => vscode.commands.executeCommand('workbench.action.navigateForward'), + }, +]; + +// COMMAND CALLBACK IMPLEMENTATIONS +function openWebview(): void { + //vscode.commands.executeCommand('workbench.action.zoomOut'); + const panel = vscode.window.createWebviewPanel( + 'mindReader', // Identifies the type of the webview. Used internally + 'Mind Reader', // Title of the panel displayed to the user + vscode.ViewColumn.One, // Editor column to show the new webview panel in. + {} + ); // Webview options. More on these later. + + panel.webview.html = getWebviewContent('media/html/main.html'); +} + +function getWebviewContent(filepath: string) { + return fs.readFileSync(filepath, {encoding: 'utf-8'}); +} + +function openKeyBindWin(os: 'Mac' | 'Windows'): void { + //vscode.commands.executeCommand('workbench.action.zoomOut'); + const panel = vscode.window.createWebviewPanel( + 'mindReader', // Identifies the type of the webview. Used internally + 'MR Key Bindings', // Title of the panel displayed to the user + vscode.ViewColumn.One, // Editor column to show the new webview panel in. + {} + ); // Webview options. More on these later. + + if (os === 'Windows') { + panel.webview.html = getWebviewContent('media/html/winkeys.html'); + } else if (os === 'Mac') { + panel.webview.html = getWebviewContent('media/html/mackeys.html'); + } +} + + diff --git a/src/commands/text.ts b/src/commands/text.ts new file mode 100755 index 0000000..8e3beaf --- /dev/null +++ b/src/commands/text.ts @@ -0,0 +1,168 @@ +import * as vscode from 'vscode'; +import * as pl from '../pylex'; + +import { CommandEntry } from './commandEntry'; + +export const textCommands: CommandEntry[] = [ + { + name: 'mind-reader.getIndent', + callback: getIndent, + }, + + { + name: 'mind-reader.runLineContext', + callback: runLineContext, + }, + + { + name: 'mind-reader.runCursorContext', + callback: runCursorContext + } +] + +function getIndent(): void { + let editor = vscode.window.activeTextEditor; + if(editor) + { + let lineNum = editor.selection.active.line + 1; + let textLine = editor.document.lineAt(lineNum - 1); + if(textLine.isEmptyOrWhitespace) + { + vscode.window.showInformationMessage("Line number " + lineNum.toString() + " Is Empty"); + } + else + { + // Grab tab format from open document + let tabFmt = { + size: editor.options.tabSize as number, + hard: !editor.options.insertSpaces + }; + let i = pl.Lexer.getIndent(textLine.text, tabFmt); + vscode.window.showInformationMessage("Line Number " + lineNum.toString() + " Indentation " + i.toString()); + } + } + else{ + vscode.window.showErrorMessage('No document currently active'); + } + +} + +function runLineContext(): void { + let editor = vscode.window.activeTextEditor; + if (editor){ + // current text and line number + let editorText = editor.document.getText(); + let line = editor.selection.active.line; + + // get tab info settings + let size = parseInt(editor.options.tabSize as string); + let hard = !editor.options.insertSpaces; + + // initialize parser + let parser = new pl.Parser(editorText, {size, hard}); + parser.parse(); + + let context = parser.context(line); + + // build text + let contentString = createContextString(context, line); + vscode.window.showInformationMessage(contentString); + } else { + vscode.window.showErrorMessage('No document currently active'); + } +} + +function createContextString(context: pl.LexNode[], line: number): string { + if (context.length < 1) { + throw new Error('Cannot create context string for empty context'); + } + + let contextString = 'Line ' + (line+1); // 1 based + if (context[0].token && context[0].token.attr) { + contextString += ': ' + context[0].token.type.toString() + ' ' + context[0].token.attr.toString(); + } + for (let i = 1; i < context.length; i++) { + let node = context[i]; + if (node.label === 'root') { + // root + contextString += ' in the Document Root'; + continue; + } + + if (node.token!.type !== pl.PylexSymbol.EMPTY && + node.token!.type !== pl.PylexSymbol.INDENT) { + contextString += ' inside ' + node.token!.type.toString(); + if (node.token!.attr) { + contextString += ' ' + node.token!.attr.toString(); + } + } + } + return contextString; +} + +// find up to `n` words around the cursor, where `n` is +// the value of `#mindReader.reader.contextWindow` +function runCursorContext(): void { + let editor = vscode.window.activeTextEditor; + if (!editor) { + vscode.window.showErrorMessage('RunCursorContext: No Active Editor'); + return; + } + + const cursorPos: vscode.Position = editor.selection.active; + const text: string = editor.document.lineAt(cursorPos).text; + const windowSize: number = vscode.workspace.getConfiguration('mindReader').get('reader.contextWindow')!; + + let trimmedText = text.trimStart(); // trim leading whitespace + let leadingWS = text.length - trimmedText.length; // # of characters of leading whitespace + trimmedText = trimmedText.trimEnd(); // trim trailing whitespace + let pos = leadingWS; + let maxPos = text.length; + + // clamp cursor start/end to new range + let col = cursorPos.character; // effective column of the cursor position + if (col < leadingWS) { + // move effective start to first non-whitespace character in the line + col = leadingWS; + } else if (col > leadingWS + trimmedText.length - 1) { + // move effective end to last non-whitespace character in the line + col = leadingWS + trimmedText.length - 1; + } + + // generate list of space separate words with range data (start, end) + // TODO: can find user position to be done in one pass + let spaceWords: {word: string, start: number, end: number}[] = []; + while (pos < maxPos && trimmedText.length > 0) { + let word = trimmedText.replace(/ .*/, ''); + spaceWords.push({word, start: pos, end: pos+word.length}); + + // remove processed word from trimmed text + const oldText = trimmedText; + trimmedText = trimmedText.replace(/[^ ]+/, '').trimStart(); + + // update pos to start of next word + pos += oldText.length - trimmedText.length; + } + + // find word the user is in + let contextStart: number = -1, contextEnd: number = -1; + for (let i = 0; i < spaceWords.length; i++) { + if (col >= spaceWords[i].start && col <= spaceWords[i].end) { + // found the word + contextStart = Math.max(0, i - windowSize); // clamp start index + contextEnd = Math.min(spaceWords.length, i + windowSize + 1); // clamp end index + + // construct cursor context string + let contextString = ''; + for (let i = contextStart; i < contextEnd; i++) { + contextString += spaceWords[i].word + ' '; + } + + // output cursor context string + vscode.window.showInformationMessage(contextString); + + return; + } + } +} + diff --git a/src/extension.ts b/src/extension.ts index ebcdb92..5496408 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,7 +1,12 @@ import * as vscode from 'vscode'; import * as pl from './pylex'; -import { accessCommands, hubCommands, navCommands } from './commands'; +import { + accessCommands, + hubCommands, + navCommands, + textCommands +} from './commands'; import CommandNodeProvider from './commandNodeProvider'; import Logger from './log'; @@ -18,7 +23,12 @@ export function activate(context: vscode.ExtensionContext) { parser.parse('Beep Boop'); - let allCommands = accessCommands.concat(hubCommands).concat(navCommands); + const allCommands = [ + accessCommands, + hubCommands, + navCommands, + textCommands + ].flat(1); // Register Commands allCommands.forEach(command => { @@ -29,7 +39,7 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(disposable); }); - let accessProvider = new CommandNodeProvider(accessCommands); + let accessProvider = new CommandNodeProvider([accessCommands, textCommands].flat(1)); vscode.window.registerTreeDataProvider('accessActions', accessProvider); let hubProvider = new CommandNodeProvider(hubCommands); diff --git a/src/hubManager.ts b/src/hubManager.ts index 522f532..8d5bf7b 100644 --- a/src/hubManager.ts +++ b/src/hubManager.ts @@ -4,6 +4,21 @@ import * as fs from 'fs'; import { logger } from './extension'; +/** + * Communication with the Hub takes place using a modified version of the JSON RPC 1.0 protocol: + * https://en.wikipedia.org/wiki/JSON-RPC + * + * It is mostly the same, except that the field names are abbreviated to the first letter. So, + * + * FIELD -> ABBREVIATION + * ------ -> ---------- + * method -> m + * params -> p + * id -> i + * result -> r + * error -> e + */ + /** * @type RPCRequest an RPC request message * @@ -21,7 +36,7 @@ type RPCRequest = { * @type RPCResponse an RPC response message * * @prop {any?} `'r'` RPC response body - * @prop {Object?} `'e'` RPC error body + * @prop {Object?} `'e'` RPC error body (base64 encoded) * @prop {string|null} `'i'` required response ID. */ type RPCResponse = {