7

Assuming I have the Delphi IDE open, how can I open a .pas file selected in another app and open it in the Delphi IDE, as well as positioning it to a specific line number?

I've seen some editing tools do this.

I'm not sure if it's just an option to a normal file open (eg., using default file association), or a command-line option, or you need DDE or COM or something entirely different.

Note that I don't want to close the project and reopen a new or fake project.

Also, I don't want the file added to the project. I just want to open it.

For example, When you <ctrl>-click on a varible or type, the IDE will open the file containing that symbol and go to the line where that symbol is declared. That's all I want to do -- but from an external app. (I'm not looking for a symbol, just a line.)

I'm using Delphi XE5 at the moment, so I'm interested in newer Delphi versions, not pre-XE2 or so.

(Part of the question is, how do I ensure that if the IDE is already open, the the file is opened in anew tab inside of the current IDE rather than in another instance of the IDE?)

David Schwartz
  • 1,756
  • 13
  • 18
  • The "already open" part may be answered by Peter Below's post in http://codeverge.com/embarcadero.delphi.ide/opening-up-a-pas-file-opens-up-another/1058202. As for the specific line no part, I'm sure someone asked about that in the past couple of weeks or so, either here, in the EMBA newsgroups or maybe the google+ Delphi community - can't remember the answer offhand though. – MartynA Jul 11 '14 at 06:14
  • Thanks for the tip to the earlier post, but I basically want to emulate the File -> Open action, which opens a file in a tab next to the current tab. I don't want to close the project and reopen a new or fake project. I don't want the file added to the project. I just want to open it. For example, When you -click on a varible or type, the IDE will open the file and go to the line where that symbol is declared. That's all I want to do -- but from an external app. (I'm not looking for a symbol, just a line.) – David Schwartz Jul 11 '14 at 06:33
  • 1
    This is the one I was thinking of : https://forums.embarcadero.com/thread.jspa?threadID=105735. In a similar vein: http://stackoverflow.com/questions/22498243/how-to-highlight-a-specific-line-in-source-editor-using-opentoolsapi. Might be best to put your "don't wants" into the q. – MartynA Jul 11 '14 at 07:01
  • Btw, if your considering doing something via OTA, this chap's blog is a bit of a goldmine; see chapter 11 in particular. – MartynA Jul 11 '14 at 07:14
  • There would have to be a command-line option available for the IDE, which is how double-clicking a .pas file in Windows Explorer works when the IDE is already open. It passes the file to the IDE command line, which starts to load, sees there's already a running instance, passes a WM_COPYDATA message to that running instance containing the filename, and closes itself. If you could intercept that WM_COPYDATA message to see what it contains, you might accomplish opening the file, but I'm not aware of any command line arg that goes to a specified line #. (continued) – Ken White Jul 11 '14 at 20:03
  • (continued) The WM_COPYDATA doesn't apply when you're in the IDE and Ctrl-click an identifier, as the IDE knows what that identifier is and where it was defined, so it can directly open the .pas file and (using the same code as in Search->Go to line) navigate directly to the line it needs. It's not receiving any of that information from an external source or via a Windows message. According to the [docwiki](http://docwiki.embarcadero.com/RADStudio/XE6/en/IDE_Command_Line_Switches_and_Options), there's no command line option to go to a specified line. – Ken White Jul 11 '14 at 20:05
  • 1
    I can't see how you can do this without going the Tools API route. – Graymatter Jul 11 '14 at 20:11
  • @KenWhite To answer a comment of yours on the deleted answer. Why can't a ToolsAPI plugin receive a message of sorts? – Graymatter Jul 11 '14 at 21:20
  • @Ken - Here, double clicking a 'pas' file opens a new instance. But maybe something is wrong with my setup. – Sertac Akyuz Jul 11 '14 at 21:39
  • @Graymatter: I didn't say it couldn't receive a message, but clearly you'd have to modify the external app to **send** that message first. The deleted answer simply mentioned using the IOTAEditView.CursorPos, but not how it would be used to do what was asked here. (It was also an addendum to the comment David H. posted to that question as well (I wrote "That also doesn't", not "You can't", if you'll recall). – Ken White Jul 11 '14 at 21:43
  • @Sertac: Hmmm... Maybe it's something wrong with *my* setup. I'll have to look at that Monday at the office; I don't know what it does at home. – Ken White Jul 11 '14 at 21:44
  • @MartynA, sorry, I didn't get it, which blog? Could you please clarify? I'm generally interested in OTA. – Free Consulting Jul 11 '14 at 22:25
  • Gentlemen, here shell open reuses a existing instance of XE2, as far as I remember, Galileo IDEs were behaving like this. – Free Consulting Jul 11 '14 at 22:28
  • 1
    @Sertac: Yep, double-clicking a .pas file in Explorer loads it into a currently open instance of the IDE at home. What does `assoc .pas`, and then `ftype` for whatever `assoc` returned show you? Mine says `BDS.PasFile="C:\RAD Studio\14.0\Bin\bdsLauncher.exe" "C:\RAD Studio\14.0\Bin\bds.exe" /np"` – Ken White Jul 11 '14 at 23:53
  • 1
    @Ken - They return the same with the difference of the path and the version, ..\9.0\Bin\bdsLauncher.exe... With the information of how it's supposed to work, I was able to correct it by deleting the '.pas' entry in `HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\`. Now .pas files opens in the current instance. Much appreciated, thank you! – Sertac Akyuz Jul 12 '14 at 00:17
  • 1
    @Free Consulting Sorry, careless me, I left the link out http://www.davidghoyle.co.uk/WordPress/?cat=3 – MartynA Jul 12 '14 at 05:24
  • Could you clarify a couple of things, please? The "another app" - is that one you've written and that you could modify to specify the line number the IDE should go to? If not, are you envisaging writing another app as intermediary between that app and the IDE, or what? – MartynA Jul 29 '14 at 09:55
  • Yes, it's an app I've written that analyzes some log files that include files and line numbers in the files that are of interest. I'd like to be able to click on a line (with filename and line#) and have it open in the IDE at that line#. – David Schwartz Jul 30 '14 at 01:22
  • In delphi 10.4 it suffices to call from a command prompt or directly from another application (I have written a tool in C# to manage a legacy Delphi package) `explorer .pas` If the IDE is running, the chosen unit will be shown, inside the project it belongs to if needed. – Francesco Iovine Mar 14 '22 at 10:49

1 Answers1

3

The code below (for D7) shows how this can be done by way of an IDE add-in .Dpk compiled into a Bpl. It started as just a "proof of concept", but it does actually work.

It comprises a "sender" application which uses WM_COPYDATA to send the FileName, LineNo & Column to a receiver hosted in the .Bpl file.

The sender sends the receiver a string like

Filename=d:\aaad7\ota\dskfilesu.pas
Line=8
Col=12
Comment=(* some comment or other*)

The Comment line is optional.

In the .Bpl, the receiver uses OTA services to open the requested file and positions the editor caret, then inserts the comment, if any.

The trickiest thing was to find out how to handle one particular complication, the case where the named file to be opened is one with an associated form. If so, in D7 (and, I assume, other IDE versions with the floating designer option enabled) when the IDE opens the .Pas file, it also opens the .Dfm, and left to its own devices, that would leave the form editor in front of the code editor. Calling the IOTASourceEditor.Show for the .Pas file at least puts the IDE code editor in front of the .Dfm form, but that didn't satisfy me, because by now my curiosity was piqued - how do you get a form the IDE is displaying off the screen?

I spent a lot of time exploring various blind alleys, because the OTA + NTA services don't seem to provide any way to explicitly close an IOTAEditor or any of its descendants. In the end it turned out that the thing to do is simply get a reference to the form and just send it a WM_CLOSE(!) - see comments in the code.

Fwiw, being a novice at OTA, at first (before I found out how IOTAModules work) I found that far and away the most difficult part of this was discovering how to get hold of the IEditView interface needed to set the editor caret position, but as usual with these interfacey things, once you get the "magic spell" exactly right, it all works.

Good luck! And thanks for the fascinating challenge!

unit Receiveru;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls, ToolsAPI;

type
  TOTAEditPosnForm = class(TForm)
    Memo1: TMemo;
  private
    FEdLine: Integer;
    FEdCol: Integer;
    FEditorFileName: String;
    FEditorInsert: String;
    procedure WMCopyData(var Msg : TWMCopyData); message WM_COPYDATA;
    procedure HandleCopyDataString(CopyDataStruct : PCopyDataStruct);
    procedure OpenInIDEEditor;
    property EditorFileName : String read FEditorFileName write FEditorFileName;
    property EdLine : Integer read FEdLine write FEdLine;
    property EdCol : Integer read FEdCol write FEdCol;
    property EditorInsert : String read FEditorInsert write FEditorInsert;
  end;

var
  OTAEditPosnForm: TOTAEditPosnForm;

procedure Register;

implementation

{$R *.dfm}

procedure MonitorFiles;
begin
  OTAEditPosnForm := TOTAEditPosnForm.Create(Nil);
  OTAEditPosnForm.Show;
end;

procedure Register;
begin
  MonitorFiles;
end;

procedure TOTAEditPosnForm.OpenInIDEEditor;
var
  IServices : IOTAServices;
  IActionServices : IOTAActionServices;
  IModuleServices : IOTAModuleServices;
  IEditorServices : IOTAEditorServices60;
  IModule : IOTAModule;
  i : Integer;
  IEditor : IOTAEditor;
  ISourceEditor : IOTASourceEditor;
  IFormEditor : IOTAFormEditor;
  IComponent : IOTAComponent;
  INTAComp : INTAComponent;
  AForm : TForm;
  IEditView : IOTAEditView;
  CursorPos : TOTAEditPos;
  IEditWriter : IOTAEditWriter;
  CharPos : TOTACharPos;
  InsertPos : Longint;
  FileName : String;
begin
  IServices := BorlandIDEServices as IOTAServices;
  Assert(Assigned(IServices), 'IOTAServices not available');

  IServices.QueryInterface(IOTAACtionServices, IActionServices);
  if IActionServices <> Nil then begin

    IServices.QueryInterface(IOTAModuleServices, IModuleServices);
    Assert(IModuleServices <> Nil);

    //  Close all files open in the IDE
    IModuleServices.CloseAll;

    if IActionServices.OpenFile(EditorFileName) then begin

      //  At this point, if the named file has an associated .DFM and
      //  we stopped here, the form designer would be in front of the
      //  code editor.

      IModule := IModuleServices.Modules[0];
      //  IModule is the one holding our .Pas file and its .Dfm, if any
      //  So, iterate the IModule's editors until we find the one
      //  for the .Pas file and then call .Show on it.  This will
      //  bring the code editor in front of the form editor.

      ISourceEditor := Nil;

      for i := 0 to IModule.ModuleFileCount - 1 do begin
        IEditor := IModule.ModuleFileEditors[i];
        FileName := IEditor.FileName;
        Memo1.Lines.Add(Format('%d %s', [i, FileName]));
        if CompareText(ExtractFileExt(IEditor.FileName), '.Pas') = 0 then begin
          if ISourceEditor = Nil then begin
            IEditor.QueryInterface(IOTASourceEditor, ISourceEditor);
            IEditor.Show;
          end
        end
        else begin
          // Maybe the editor is a Form Editor.  If it is
          // close the form (the counterpart to the .Pas, that is}
          IEditor.QueryInterface(IOTAFormEditor, IFormEditor);
          if IFormEditor <> Nil then begin
            IComponent := IFormEditor.GetRootComponent;
            IComponent.QueryInterface(INTAComponent, INTAComp);
            AForm := TForm(INTAComp.GetComponent);
            //AForm.Close; < this does NOT close the on-screen form
            // IActionServices.CloseFile(IEditor.FileName); <- neither does this
            SendMessage(AForm.Handle, WM_Close, 0, 0);  // But this does !
          end;
        end;
      end;

      //  Next, place the editor caret where we want it ...
      IServices.QueryInterface(IOTAEditorServices, IEditorServices);
      Assert(IEditorServices <> Nil);

      IEditView := IEditorServices.TopView;
      Assert(IEditView <> Nil);
      CursorPos.Line := edLine;
      CursorPos.Col := edCol;
      IEditView.SetCursorPos(CursorPos);
      //  and scroll the IEditView to the caret
      IEditView.MoveViewToCursor;

      //  Finally, insert the comment, if any
      if EditorInsert <> '' then begin
        Assert(ISourceEditor <> Nil);
        IEditView.ConvertPos(True, CursorPos, CharPos);
        InsertPos := IEditView.CharPosToPos(CharPos);
        IEditWriter := ISourceEditor.CreateUndoableWriter;
        Assert(IEditWriter <> Nil, 'IEditWriter');
        IEditWriter.CopyTo(InsertPos);
        IEditWriter.Insert(PChar(EditorInsert));
        IEditWriter := Nil;
      end;
    end;
  end;
end;

procedure TOTAEditPosnForm.HandleCopyDataString(
  CopyDataStruct: PCopyDataStruct);
begin
  Memo1.Lines.Text := PChar(CopyDataStruct.lpData);

  EditorFileName := Memo1.Lines.Values['FileName'];
  edLine := StrToInt(Memo1.Lines.Values['Line']);
  edCol := StrToInt(Memo1.Lines.Values['Col']);
  EditorInsert := Trim(Memo1.Lines.Values['Comment']);
  if EditorFileName <> '' then
    OpenInIDEEditor;
end;

procedure TOTAEditPosnForm.WMCopyData(var Msg: TWMCopyData);
begin
  HandleCopyDataString(Msg.CopyDataStruct);
  msg.Result := Length(Memo1.Lines.Text);
end;

initialization

finalization
  if Assigned(OTAEditPosnForm) then begin
    OTAEditPosnForm.Close;
    FreeAndNil(OTAEditPosnForm);
  end;
end.

Code for sender:

procedure TSenderMainForm.btnSendClick(Sender: TObject);
begin
  SendMemo;
end;

procedure TSenderMainForm.SendData(
  CopyDataStruct: TCopyDataStruct);
var
  HReceiver : THandle;
  Res : integer;
begin
  HReceiver := FindWindow(PChar('TOTAEditPosnForm'),PChar('OTAEditPosnForm'));
  if HReceiver = 0 then begin
    Caption := 'CopyData Receiver NOT found!';
  end
  else begin
    Res := SendMessage(HReceiver, WM_COPYDATA, Integer(Handle), Integer(@CopyDataStruct));
    if Res > 0 then
      Caption := Format('Received %d characters', [Res]);
  end;
end;

procedure TSenderMainForm.SendMemo;
var
  MS : TMemoryStream;
  CopyDataStruct : TCopyDataStruct;
  S : String;
begin
  MS := TMemoryStream.Create;
  try
    S := Memo1.Lines.Text + #0;
    MS.Write(S[1], Length(S));
    CopyDataStruct.dwData := 1;
    CopyDataStruct.cbData := MS.Size;
    CopyDataStruct.lpData := MS.Memory;
    SendData(CopyDataStruct);
  finally
    MS.Free;
  end;
end;
MartynA
  • 30,454
  • 4
  • 32
  • 73
  • I've replaced the original "shared file" implementation with one which uses WM_CopyData to send the file details. A bit to my surprise, this shortened the receiver's code quite a bit. – MartynA Dec 08 '14 at 09:11
  • @DavidSchwartz: I finally found out how to close the form defined in the .Dfm counterpart to the .Pas file and added that to my answer, so I think I'm done for now. – MartynA Dec 11 '14 at 11:44