From 941868f157e71a3c8d2ed41efdfcfab772e88104 Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Thu, 3 Dec 2020 11:58:04 -0600 Subject: [PATCH] feat: add tree view context menu --- menus/terminal.json | 18 ++--- spec/model-spec.js | 1 + spec/terminal-spec.js | 27 +++++++ src/model.ts | 53 +++++++------- src/terminal.ts | 160 +++++++++++++++++++++++++++++++++++++----- 5 files changed, 207 insertions(+), 52 deletions(-) diff --git a/menus/terminal.json b/menus/terminal.json index 95a7df8..e64d9d0 100644 --- a/menus/terminal.json +++ b/menus/terminal.json @@ -32,41 +32,41 @@ "command": "terminal:close-all" } ], - "atom-text-editor": [ + "atom-text-editor, .tree-view, .tab-bar": [ { "label": "Terminal", "submenu": [ { "label": "Open New Terminal", - "command": "terminal:open" + "command": "terminal:open-context-menu" }, { "label": "Open New Terminal (Split Up)", - "command": "terminal:open-up" + "command": "terminal:open-up-context-menu" }, { "label": "Open New Terminal (Split Down)", - "command": "terminal:open-down" + "command": "terminal:open-down-context-menu" }, { "label": "Open New Terminal (Split Left)", - "command": "terminal:open-left" + "command": "terminal:open-left-context-menu" }, { "label": "Open New Terminal (Split Right)", - "command": "terminal:open-right" + "command": "terminal:open-right-context-menu" }, { "label": "Open New Terminal (Bottom Dock)", - "command": "terminal:open-bottom-dock" + "command": "terminal:open-bottom-dock-context-menu" }, { "label": "Open New Terminal (Left Dock)", - "command": "terminal:open-left-dock" + "command": "terminal:open-left-dock-context-menu" }, { "label": "Open New Terminal (Right Dock)", - "command": "terminal:open-right-dock" + "command": "terminal:open-right-dock-context-menu" }, { "label": "Close All Terminals", diff --git a/spec/model-spec.js b/spec/model-spec.js index ad412cf..437c007 100644 --- a/spec/model-spec.js +++ b/spec/model-spec.js @@ -89,6 +89,7 @@ describe("TerminalModel", () => { it("constructor with previous active item which exists in project path", async () => { const previousActiveItem = jasmine.createSpyObj("somemodel", ["getPath"]) + previousActiveItem.getPath.and.returnValue("/some/dir/file") spyOn(atom.workspace, "getActivePaneItem").and.returnValue(previousActiveItem) const expected = ["/some/dir", null] spyOn(atom.project, "relativizePath").and.returnValue(expected) diff --git a/spec/terminal-spec.js b/spec/terminal-spec.js index da6a84f..ed52e05 100644 --- a/spec/terminal-spec.js +++ b/spec/terminal-spec.js @@ -70,4 +70,31 @@ describe("terminal", () => { expect(newTerminal.runCommand).toHaveBeenCalledWith("command 2") }) }) + + describe('open()', () => { + let uri + beforeEach(() => { + uri = terminal.generateNewUri() + spyOn(atom.workspace, 'open') + }) + + it('simple', async () => { + await terminal.open(uri) + + expect(atom.workspace.open).toHaveBeenCalledWith(uri, {}) + }) + + it('target to cwd', async () => { + const testPath = '/test/path' + spyOn(terminal, 'getPath').and.returnValue(testPath) + await terminal.open( + uri, + { target: true }, + ) + + const url = new URL(atom.workspace.open.calls.mostRecent().args[0]) + + expect(url.searchParams.get('cwd')).toBe(testPath) + }) + }) }) diff --git a/src/model.ts b/src/model.ts index 084f345..28a76d0 100644 --- a/src/model.ts +++ b/src/model.ts @@ -36,6 +36,7 @@ export class TerminalModel { this.uri = uri const url = new URL(this.uri) this.sessionId = url.host + this.cwd = url.searchParams.get('cwd') || undefined this.terminalsSet = terminalsSet this.activeIndex = this.terminalsSet.size this.title = DEFAULT_TITLE @@ -55,41 +56,45 @@ export class TerminalModel { } async initialize() { - this.cwd = await this.getInitialCwd() + if (!this.cwd) { + this.cwd = await this.getInitialCwd() + } } - async getInitialCwd() { + async getInitialCwd(): Promise { const previousActiveItem = atom.workspace.getActivePaneItem() // @ts-ignore let cwd = previousActiveItem?.getPath?.() - const dir = atom.project.relativizePath(cwd)[0] - if (dir) { - // Use project paths whenever they are available by default. - return dir + if (cwd) { + const dir = atom.project.relativizePath(cwd)[0] + if (dir) { + // Use project paths whenever they are available by default. + return dir + } } try { - // Otherwise, if the path exists on the local file system, use the - // path or parent directory as appropriate. - const stats = await fs.stat(cwd) - if (stats.isDirectory()) { - return cwd - } + if (cwd) { + // Otherwise, if the path exists on the local file system, use the + // path or parent directory as appropriate. + const stats = await fs.stat(cwd) + if (stats.isDirectory()) { + return cwd + } - cwd = path.dirname(cwd) - const dirStats = await fs.stat(cwd) - if (dirStats.isDirectory()) { - return cwd + cwd = path.dirname(cwd) + const dirStats = await fs.stat(cwd) + if (dirStats.isDirectory()) { + return cwd + } } - } catch {} + } catch { + //failt silently + } cwd = atom.project.getPaths()[0] // no project paths - if (cwd) { - return cwd - } - - return + return cwd } serialize() { @@ -126,7 +131,7 @@ export class TerminalModel { return DEFAULT_TITLE + " (" + this.title + ")" } - onDidChangeTitle(callback: (value?: any) => void) { + onDidChangeTitle(callback: (value?: string) => void) { return this.emitter.on("did-change-title", callback) } @@ -142,7 +147,7 @@ export class TerminalModel { return this.modified } - onDidChangeModified(callback: (value?: any) => void) { + onDidChangeModified(callback: (value?: boolean) => void) { return this.emitter.on("did-change-modified", callback) } diff --git a/src/terminal.ts b/src/terminal.ts index 76a854e..1eb8e16 100644 --- a/src/terminal.ts +++ b/src/terminal.ts @@ -1,4 +1,10 @@ -import { CompositeDisposable, Workspace, Dock, WorkspaceOpenOptions } from "atom" +import { + CompositeDisposable, + Workspace, + Dock, + Pane, + WorkspaceOpenOptions, +} from "atom" import { TerminalElement } from "./element" import { TerminalModel } from "./model" @@ -6,6 +12,11 @@ export * from "./button" import { v4 as uuidv4 } from "uuid" +interface OpenOptions extends WorkspaceOpenOptions { + target?: EventTarget | null, + pane?: Pane, +} + const TERMINAL_BASE_URI = "terminal://" class Terminal { @@ -109,6 +120,72 @@ class Terminal { "terminal:close-all": () => this.exitAllTerminals(), "terminal:focus": () => this.focus(), }), + // @ts-ignore + atom.commands.add("atom-text-editor, .tree-view, .tab-bar", { + 'terminal:open-context-menu': { + hiddenInCommandPalette: true, + didDispatch: ({ target }) => this.open( + this.generateNewUri(), + this.addDefaultPosition({ target }), + ), + }, + 'terminal:open-center-context-menu': { + hiddenInCommandPalette: true, + didDispatch: ({ target }) => this.openInCenterOrDock( + atom.workspace, + { target }, + ), + }, + 'terminal:open-split-up-context-menu': { + hiddenInCommandPalette: true, + didDispatch: ({ target }) => this.open( + this.generateNewUri(), + { split: 'up', target }, + ), + }, + 'terminal:open-split-down-context-menu': { + hiddenInCommandPalette: true, + didDispatch: ({ target }) => this.open( + this.generateNewUri(), + { split: 'down', target }, + ), + }, + 'terminal:open-split-left-context-menu': { + hiddenInCommandPalette: true, + didDispatch: ({ target }) => this.open( + this.generateNewUri(), + { split: 'left', target }, + ), + }, + 'terminal:open-split-right-context-menu': { + hiddenInCommandPalette: true, + didDispatch: ({ target }) => this.open( + this.generateNewUri(), + { split: 'right', target }, + ), + }, + 'terminal:open-split-bottom-dock-context-menu': { + hiddenInCommandPalette: true, + didDispatch: ({ target }) => this.openInCenterOrDock( + atom.workspace.getBottomDock(), + { target }, + ), + }, + 'terminal:open-split-left-dock-context-menu': { + hiddenInCommandPalette: true, + didDispatch: ({ target }) => this.openInCenterOrDock( + atom.workspace.getLeftDock(), + { target }, + ), + }, + 'terminal:open-split-right-dock-context-menu': { + hiddenInCommandPalette: true, + didDispatch: ({ target }) => this.openInCenterOrDock( + atom.workspace.getRightDock(), + { target }, + ), + }, + }), atom.commands.add("atom-terminal", { "terminal:close": () => this.close(), "terminal:restart": () => this.restart(), @@ -127,7 +204,7 @@ class Terminal { this.disposables.dispose() } - deserializeTerminalModel(serializedModel: {uri: string}) { + deserializeTerminalModel(serializedModel: TerminalModel) { if (atom.config.get("terminal.allowRelaunchingTerminalsOnStartup")) { return new TerminalModel({ uri: serializedModel.uri, @@ -137,10 +214,9 @@ class Terminal { return } - openInCenterOrDock(centerOrDock: Workspace | Dock, options: WorkspaceOpenOptions = {}) { + openInCenterOrDock(centerOrDock: Workspace | Dock, options: OpenOptions = {}) { const pane = centerOrDock.getActivePane() if (pane) { - // @ts-ignore options.pane = pane } return this.open(this.generateNewUri(), options) @@ -157,9 +233,20 @@ class Terminal { return terminals.find((t) => t.isActiveTerminal()) } - async open(uri: string, options = {}): Promise { + async open(uri: string, options: OpenOptions = {}): Promise { // TODO: should we check uri for TERMINAL_BASE_URI? - return >atom.workspace.open(uri, options) + // if (!uri.startsWith(TERMINAL_BASE_URI)) { + // return null + // } + + const url = new URL(uri) + if (options.target) { + const target = this.getPath(options.target) + if (target) { + url.searchParams.set('cwd', target) + } + } + return >atom.workspace.open(url.href, options) } generateNewUri() { @@ -174,7 +261,7 @@ class Terminal { * @param {Object} options Options to pass to call to 'atom.workspace.open()'. * @return {TerminalModel} Instance of TerminalModel. */ - async openTerminal(options: WorkspaceOpenOptions = {}): Promise { + async openTerminal(options: OpenOptions = {}): Promise { options = this.addDefaultPosition(options) return this.open(this.generateNewUri(), options) } @@ -208,13 +295,12 @@ class Terminal { return terminal } - addDefaultPosition(options: WorkspaceOpenOptions = {}): WorkspaceOpenOptions { + addDefaultPosition(options: OpenOptions = {}): OpenOptions { const position = atom.config.get("terminal.defaultOpenPosition") switch (position) { case "Center": { const pane = atom.workspace.getActivePane() if (pane && !("pane" in options)) { - // @ts-ignore options.pane = pane } break @@ -242,7 +328,6 @@ class Terminal { case "Bottom Dock": { const pane = atom.workspace.getBottomDock().getActivePane() if (pane && !("pane" in options)) { - // @ts-ignore options.pane = pane } break @@ -250,7 +335,6 @@ class Terminal { case "Left Dock": { const pane = atom.workspace.getLeftDock().getActivePane() if (pane && !("pane" in options)) { - // @ts-ignore options.pane = pane } break @@ -258,7 +342,6 @@ class Terminal { case "Right Dock": { const pane = atom.workspace.getRightDock().getActivePane() if (pane && !("pane" in options)) { - // @ts-ignore options.pane = pane } break @@ -267,11 +350,53 @@ class Terminal { return options } + getPath (target: EventTarget | null) : string | null | undefined { + if (!target || !(target instanceof HTMLElement)) { + const paths = atom.project.getPaths() + if (paths && paths.length > 0) { + return paths[0] + } + return null + } + + const treeView = target.closest('.tree-view') + if (treeView) { + // called from treeview + const selected = treeView.querySelector('.selected > .list-item > .name, .selected > .name') as HTMLElement + if (selected) { + return selected.dataset.path + } + return null + } + + const tab = target.closest('.tab-bar > .tab') + if (tab) { + // called from tab + const title = tab.querySelector('.title') as HTMLElement + if (title && title.dataset.path) { + return title.dataset.path + } + return null + } + + const textEditor = target.closest('atom-text-editor') + if (textEditor && typeof textEditor.getModel === 'function') { + // called from atom-text-editor + const model = textEditor.getModel() + if (model && typeof model.getPath === 'function') { + const modelPath = model.getPath() + if (modelPath) { + return modelPath + } + } + return null + } + + return null + } + /** * Function providing service functions offered by 'terminal' service. - * - * @function - * @returns {Object} Object holding service functions. */ provideTerminalService() { // TODO: provide other service functions @@ -280,9 +405,6 @@ class Terminal { /** * Function providing service functions offered by 'platformioIDETerminal' service. - * - * @function - * @returns {Object} Object holding service functions. */ providePlatformIOIDEService() { return { @@ -371,7 +493,7 @@ export function deactivate(): void { } } -export function deserializeTerminalModel(serializedModel: {uri: string}) { +export function deserializeTerminalModel(serializedModel: TerminalModel) { return getInstance().deserializeTerminalModel(serializedModel) }