@@ -36,6 +36,7 @@ import * as Persistence from '../../models/persistence/persistence.js';
3636import * as TextUtils from '../../models/text_utils/text_utils.js' ;
3737import * as Workspace from '../../models/workspace/workspace.js' ;
3838import * as CodeMirror from '../../third_party/codemirror.next/codemirror.next.js' ;
39+ import * as Adorners from '../../ui/components/adorners/adorners.js' ;
3940import * as IconButton from '../../ui/components/icon_button/icon_button.js' ;
4041import * as IssueCounter from '../../ui/components/issue_counter/issue_counter.js' ;
4142import * as SourceFrame from '../../ui/legacy/components/source_frame/source_frame.js' ;
@@ -78,6 +79,7 @@ export class UISourceCodeFrame extends
7879 // recreated when the binding changes
7980 private plugins : Plugin [ ] = [ ] ;
8081 private readonly errorPopoverHelper : UI . PopoverHelper . PopoverHelper ;
82+ private openInExternalEditorToolbarButton ?: UI . Toolbar . ToolbarButton ;
8183 #sourcesPanelOpenedMetricsRecorded = false ;
8284
8385 constructor ( uiSourceCode : Workspace . UISourceCode . UISourceCode ) {
@@ -93,6 +95,37 @@ export class UISourceCodeFrame extends
9395 this . uiSourceCodeEventListeners = [ ] ;
9496 this . messageAndDecorationListeners = [ ] ;
9597
98+ if ( this . canOpenInExternalEditor ( ) ) {
99+ this . openInExternalEditorToolbarButton =
100+ new UI . Toolbar . ToolbarButton ( 'Open in editor' , undefined , 'Open in editor' ) ;
101+ const maybeBackgroundImage = globalThis . reactNativeOpenInEditorButtonImage ;
102+ if ( typeof maybeBackgroundImage === 'string' && maybeBackgroundImage !== '' ) {
103+ const adorner = new Adorners . Adorner . Adorner ( ) ;
104+ adorner . classList . add ( 'open-in-external-editor-adorner' ) ;
105+ adorner . style . setProperty ( 'background-image' , maybeBackgroundImage ) ;
106+ this . openInExternalEditorToolbarButton . element . classList . add (
107+ 'toolbar-has-glyph' , 'open-in-external-editor-button' ) ;
108+ this . openInExternalEditorToolbarButton . setGlyphOrAdorner ( adorner ) ;
109+ } else {
110+ this . openInExternalEditorToolbarButton . setGlyph ( 'open-externally' ) ;
111+ }
112+ this . openInExternalEditorToolbarButton . addEventListener ( UI . Toolbar . ToolbarButton . Events . Click , ( ) => {
113+ const body : { url : string , lineNumber ?: number } = {
114+ url : this . uiSourceCode ( ) . url ( ) ,
115+ } ;
116+
117+ const state = this . textEditor . state ;
118+ const line = state . doc . lineAt ( state . selection . main . head ) ;
119+ const { lineNumber} = this . editorLocationToUILocation ( line . number ) ;
120+ body . lineNumber = lineNumber ;
121+
122+ fetch ( '/open-stack-frame' , {
123+ method : 'POST' ,
124+ body : JSON . stringify ( body ) ,
125+ } ) . catch ( e => console . error ( e ) ) ;
126+ } ) ;
127+ }
128+
96129 this . boundOnBindingChanged = this . onBindingChanged . bind ( this ) ;
97130
98131 Common . Settings . Settings . instance ( )
@@ -108,6 +141,14 @@ export class UISourceCodeFrame extends
108141 this . initializeUISourceCode ( ) ;
109142 }
110143
144+ private canOpenInExternalEditor ( ) : boolean {
145+ if ( ! globalThis . enableReactNativeOpenInExternalEditor ) {
146+ return false ;
147+ }
148+
149+ return this . uiSourceCode ( ) . url ( ) . startsWith ( 'http' ) ?? false ;
150+ }
151+
111152 private async workingCopy ( ) : Promise < TextUtils . ContentProvider . DeferredContent > {
112153 if ( this . uiSourceCodeInternal . isDirty ( ) ) {
113154 return { content : this . uiSourceCodeInternal . workingCopy ( ) , isEncoded : false } ;
@@ -456,6 +497,10 @@ export class UISourceCodeFrame extends
456497 return leftToolbarItems ;
457498 }
458499
500+ if ( this . openInExternalEditorToolbarButton ) {
501+ leftToolbarItems . push ( this . openInExternalEditorToolbarButton ) ;
502+ }
503+
459504 return [ ...leftToolbarItems , new UI . Toolbar . ToolbarSeparator ( true ) , ...rightToolbarItems ] ;
460505 }
461506
0 commit comments