dsh-vscode-workbench 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,14 +15,20 @@ interface Tab extends FileDocument {
15
15
  }
16
16
  /** Return the basename shown in a tab while preserving its full internal path. */
17
17
  export declare function tabLabel(tab: Pick<Tab, 'path' | 'title' | 'diffSourcePath'>): string;
18
- interface GraphRow {
18
+ export interface GraphTrack {
19
+ hash: string;
20
+ color: number;
21
+ }
22
+ export interface GraphRow {
19
23
  commit: GitCommit;
20
24
  lane: number;
21
- before: string[];
22
- after: string[];
25
+ before: GraphTrack[];
26
+ after: GraphTrack[];
23
27
  parentLanes: number[];
24
28
  }
25
29
  export declare function graphRows(commits: GitCommit[]): GraphRow[];
30
+ /** Draw a fixed-radius, orthogonal transition between two graph lanes. */
31
+ export declare function graphTrackPath(from: number, to: number, startY: number, endY: number, bendY: number): string;
26
32
  export declare function Launcher({ wide, service }: {
27
33
  wide: boolean;
28
34
  service: VscodeWorkbench;
@@ -0,0 +1,3 @@
1
+ import type { VscodeWorkbench } from './service.ts';
2
+ /** Handle terminal shortcuts through contributions registered by a terminal adapter. */
3
+ export declare function handleTerminalShortcut(event: KeyboardEvent, workbench: VscodeWorkbench): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vscode-workbench",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Explorer, Monaco editor, search and local Git overlay for DSH",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -74,38 +74,55 @@ function Search(props: { sessionId: string; onOpen(path: string, line: number, c
74
74
  return <><div className="dvw-side-head">搜索</div><form className="dvw-search-form" onSubmit={event => { event.preventDefault(); void search() }}><input className="dvw-input" value={query} onChange={e => setQuery(e.target.value)} placeholder="搜索" autoFocus/><div className="dvw-options">{([['caseSensitive','Aa'],['wholeWord','Ab'],['regex','.*']] as const).map(([key,label]) => <label key={key}><input type="checkbox" checked={flags[key]} onChange={e => setFlags({ ...flags, [key]: e.target.checked })}/>{label}</label>)}<button className="dvw-icon" title="搜索" disabled={busy}><Icon name="search"/></button></div><input className="dvw-input" value={include} onChange={e=>setInclude(e.target.value)} placeholder="包含文件,例如 src/**"/><input className="dvw-input" value={exclude} onChange={e=>setExclude(e.target.value)} placeholder="排除文件,例如 **/*.test.ts"/></form>{error&&<div className="dvw-error">{error}</div>}{limited&&<div className="dvw-message">结果已截断到 2000 条</div>}<div className="dvw-results">{[...groups].map(([path, rows]) => <div key={path}><div className="dvw-result-file"><Icon name="chevron-down"/> {path} <span className="dvw-badge">{rows.length}</span></div>{rows.map((row,index)=><div className="dvw-result" key={`${row.line}:${row.column}:${index}`} onClick={()=>props.onOpen(path,row.line,row.column)}><span className="dvw-line">{row.line}</span><span className="dvw-preview">{row.preview}</span></div>)}</div>)}</div></>
75
75
  }
76
76
 
77
- interface GraphRow { commit: GitCommit; lane: number; before: string[]; after: string[]; parentLanes: number[] }
77
+ export interface GraphTrack { hash: string; color: number }
78
+ export interface GraphRow { commit: GitCommit; lane: number; before: GraphTrack[]; after: GraphTrack[]; parentLanes: number[] }
78
79
  export function graphRows(commits: GitCommit[]): GraphRow[] {
79
- const visible=new Set(commits.map(commit=>commit.hash))
80
- let lanes: string[] = commits[0] ? [commits[0].hash] : []
80
+ const visible = new Set(commits.map(commit => commit.hash))
81
+ let nextColor = 0
82
+ let lanes: GraphTrack[] = commits[0] ? [{ hash: commits[0].hash, color: nextColor++ }] : []
81
83
  return commits.map(commit => {
82
- let lane = lanes.indexOf(commit.hash)
83
- if (lane < 0) { lanes=[commit.hash,...lanes]; lane = 0 }
84
- const before=[...lanes];const after=before.filter(hash=>hash!==commit.hash)
85
- const parents=commit.parents.filter(parent=>visible.has(parent));parents.forEach((parent,index)=>{if(!after.includes(parent))after.splice(Math.min(lane+index,after.length),0,parent)})
86
- const parentLanes=parents.map(parent=>after.indexOf(parent)).filter(index=>index>=0)
87
- lanes=after
88
- return {commit,lane,before,after,parentLanes}
84
+ let lane = lanes.findIndex(track => track.hash === commit.hash)
85
+ if (lane < 0) { lanes = [{ hash: commit.hash, color: nextColor++ }, ...lanes]; lane = 0 }
86
+ const before = [...lanes]
87
+ const current = before[lane]!
88
+ const after = before.filter(track => track.hash !== commit.hash)
89
+ const parents = commit.parents.filter(parent => visible.has(parent))
90
+ parents.forEach((parent, index) => {
91
+ if (after.some(track => track.hash === parent)) return
92
+ const color = index === 0 ? current.color : nextColor++
93
+ after.splice(Math.min(lane + index, after.length), 0, { hash: parent, color })
94
+ })
95
+ const parentLanes = parents.map(parent => after.findIndex(track => track.hash === parent)).filter(index => index >= 0)
96
+ lanes = after
97
+ return { commit, lane, before, after, parentLanes }
89
98
  })
90
99
  }
91
100
 
92
101
  const GRAPH_COLORS = ['#4c8dff','#d94f9f','#4fc1b0','#d9a441','#9b7bea']
93
- function graphColor(lane:number):string{return GRAPH_COLORS[lane%GRAPH_COLORS.length]!}
102
+ function graphColor(color:number):string{return GRAPH_COLORS[color%GRAPH_COLORS.length]!}
94
103
  function graphWidth(row:GraphRow):number{return Math.max(row.before.length,row.after.length,row.lane+1,1)*14+8}
95
104
 
105
+ /** Draw a fixed-radius, orthogonal transition between two graph lanes. */
106
+ export function graphTrackPath(from: number, to: number, startY: number, endY: number, bendY: number): string {
107
+ if (from === to) return `M ${from} ${startY} V ${endY}`
108
+ const direction = Math.sign(to - from)
109
+ const radius = Math.min(5, Math.abs(to - from) / 2, Math.abs(bendY - startY), Math.abs(endY - bendY))
110
+ return `M ${from} ${startY} V ${bendY - radius} Q ${from} ${bendY} ${from + direction * radius} ${bendY} H ${to - direction * radius} Q ${to} ${bendY} ${to} ${bendY + radius} V ${endY}`
111
+ }
112
+
96
113
  function GraphTracks({row}:{row:GraphRow}) {
97
- const width=graphWidth(row);const x=row.lane*14+8
114
+ const width=graphWidth(row);const x=row.lane*14+8;const current=row.before[row.lane]!
98
115
  return <svg className="dvw-graph-svg" width={width} height="38" viewBox={`0 0 ${width} 38`} aria-hidden="true">
99
- {row.before.map((hash,lane)=>{if(hash===row.commit.hash)return null;const target=row.after.indexOf(hash);const nextLane=target>=0?target:lane;const next=nextLane*14+8;const start=lane*14+8;return <path key={`continue-${hash}`} d={`M ${start} 0 C ${start} 14 ${next} 24 ${next} 38`} fill="none" stroke={graphColor(nextLane)} strokeWidth="2"/>})}
100
- <path d={`M ${x} 0 V 19`} fill="none" stroke={graphColor(row.lane)} strokeWidth="2"/>
101
- {row.parentLanes.map((parent,index)=>{const target=parent*14+8;return <path key={`parent-${parent}-${index}`} d={`M ${x} 19 C ${x} 27 ${target} 30 ${target} 38`} fill="none" stroke={graphColor(parent)} strokeWidth="2"/>})}
102
- <circle cx={x} cy="19" r="5" fill="var(--dvw-surface-1)" stroke={graphColor(row.lane)} strokeWidth="2"/>
116
+ {row.before.map((track,lane)=>{if(track.hash===row.commit.hash)return null;const target=row.after.findIndex(next=>next.hash===track.hash);const nextLane=target>=0?target:lane;return <path key={`continue-${track.hash}`} d={graphTrackPath(lane*14+8,nextLane*14+8,0,38,19)} fill="none" stroke={graphColor(track.color)} strokeWidth="2" vectorEffect="non-scaling-stroke"/>})}
117
+ <path d={`M ${x} 0 V 19`} fill="none" stroke={graphColor(current.color)} strokeWidth="2" vectorEffect="non-scaling-stroke"/>
118
+ {row.parentLanes.map((parent,index)=>{const target=parent*14+8;const track=row.after[parent]!;return <path key={`parent-${track.hash}-${index}`} d={graphTrackPath(x,target,19,38,29)} fill="none" stroke={graphColor(track.color)} strokeWidth="2" vectorEffect="non-scaling-stroke"/>})}
119
+ <circle cx={x} cy="19" r="5" fill="var(--dvw-surface-1)" stroke={graphColor(current.color)} strokeWidth="2" vectorEffect="non-scaling-stroke"/>
103
120
  </svg>
104
121
  }
105
122
 
106
123
  function GraphContinuation({row}:{row:GraphRow}) {
107
124
  const width=graphWidth(row)
108
- return <svg className="dvw-graph-continuation" width={width} height="100%" viewBox={`0 0 ${width} 100`} preserveAspectRatio="none" aria-hidden="true">{row.after.map((hash,lane)=><line key={hash} x1={lane*14+8} y1="0" x2={lane*14+8} y2="100" stroke={graphColor(lane)} strokeWidth="2" vectorEffect="non-scaling-stroke"/>)}</svg>
125
+ return <svg className="dvw-graph-continuation" width={width} height="100%" viewBox={`0 0 ${width} 100`} preserveAspectRatio="none" aria-hidden="true">{row.after.map((track,lane)=><line key={track.hash} x1={lane*14+8} y1="0" x2={lane*14+8} y2="100" stroke={graphColor(track.color)} strokeWidth="2" vectorEffect="non-scaling-stroke"/>)}</svg>
109
126
  }
110
127
 
111
128
  function CommitGraph({ commits, expanded, files, onToggle, onOpenFile }: { commits: GitCommit[]; expanded?: string; files: Map<string,GitCommitFile[]>; onToggle(commit:GitCommit):void; onOpenFile(commit:GitCommit,file:GitCommitFile):void }) {
@@ -8,6 +8,7 @@ import type { VscodeWorkbench } from './service.ts'
8
8
  import { WorkbenchController } from './service.ts'
9
9
  import { Launcher, Workbench } from './Workbench.tsx'
10
10
  import { installStyles } from './styles.ts'
11
+ import { handleTerminalShortcut } from './shortcuts.ts'
11
12
 
12
13
  export type { BottomPanelContribution, BottomPanelProps, VscodeWorkbench, WorkbenchCommand, WorkbenchSnapshot, WorkbenchView } from './service.ts'
13
14
 
@@ -33,6 +34,7 @@ export function apply(ctx: ClientContext): void {
33
34
  ctx.slots.inject('shell.overlay', () => ctx.slots.register({ name: 'shell.overlay', id: 'dsh-vscode-workbench-overlay', inject: () => ({ service }) }, Overlay))
34
35
  ctx.effect(() => {
35
36
  const onKey = (event: KeyboardEvent) => {
37
+ if (handleTerminalShortcut(event, service)) return
36
38
  if (event.isComposing || !event.ctrlKey || event.altKey) return
37
39
  const view = event.shiftKey && event.code === 'KeyE' ? 'explorer' : event.shiftKey && event.code === 'KeyF' ? 'search' : event.shiftKey && event.code === 'KeyG' ? 'git' : undefined
38
40
  if (view !== undefined) { event.preventDefault(); service.show(view) }
@@ -0,0 +1,17 @@
1
+ import type { VscodeWorkbench } from './service.ts'
2
+
3
+ /** Handle terminal shortcuts through contributions registered by a terminal adapter. */
4
+ export function handleTerminalShortcut(event: KeyboardEvent, workbench: VscodeWorkbench): boolean {
5
+ if (event.isComposing || !event.ctrlKey || event.altKey || event.code !== 'Backquote') return false
6
+ if (!workbench.getPanels().some(panel => panel.id === 'terminal')) return false
7
+ event.preventDefault()
8
+ event.stopImmediatePropagation()
9
+ workbench.show()
10
+ if (event.shiftKey) {
11
+ if (workbench.getSnapshot().bottomPanel !== 'terminal') workbench.toggleBottomPanel('terminal')
12
+ queueMicrotask(() => workbench.runCommand('terminal.new'))
13
+ } else {
14
+ workbench.toggleBottomPanel('terminal')
15
+ }
16
+ return true
17
+ }