Skip to content
This repository was archived by the owner on Apr 12, 2026. It is now read-only.
/ nppFSIPlugin Public archive
forked from ppv/NPPFSIPlugin

Commit 798f098

Browse files
committed
New feature: remember console input history
1 parent f2c029d commit 798f098

3 files changed

Lines changed: 213 additions & 13 deletions

File tree

Source/Plugin/Forms/FSIHostForm.pas

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ procedure TFrmFSIHost.DoClose(var Action: TCloseAction);
218218
// make sure the configuration reflects any changed options
219219
// so they're saved to disk
220220
_fsiViewer.Config.LoadFromConfigFile;
221+
_fsiViewer.Logger.ToFile;
221222
Npp.HideDialog(Handle);
222223
inherited;
223224

Source/Plugin/Src/CustomLogger.pas

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
unit CustomLogger;
2+
3+
// =============================================================================
4+
// This file is part of the F# Interactive plugin for Notepad++
5+
//
6+
// Copyright 2024 Robert Di Pardo
7+
//
8+
// This program is free software: you can redistribute it and/or
9+
// modify it under the terms of the GNU General Public License
10+
// as published by the Free Software Foundation, either version
11+
// 3 of the License, or (at your option) any later version.
12+
//
13+
// This program is distributed in the hope that it will be
14+
// useful, but WITHOUT ANY WARRANTY; without even the implied
15+
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
16+
// PURPOSE. See the GNU General Public License for more details.
17+
//
18+
// You should have received a copy of the GNU General
19+
// Public License along with this program. If not, see
20+
// <https://www.gnu.org/licenses/>.
21+
// =============================================================================
22+
23+
interface
24+
25+
uses Classes;
26+
27+
type
28+
TScrollDirection = ( sdBack, sdForward );
29+
{ Extends TStringList with simple bidirectional searching. }
30+
TCustomLogger = class(TStringList)
31+
private
32+
FStringsFilePath: WideString;
33+
FCursor: integer;
34+
function GetStr(const Index: Integer): String;
35+
procedure FromFile(const FilePath: WideString);
36+
public
37+
constructor Create; overload;
38+
constructor Create(const FilePath: WideString); overload;
39+
function Add(const Item: String): integer; override;
40+
function Scroll(Direction: TScrollDirection): String;
41+
procedure ToFile;
42+
property Log[Index: integer]: String read GetStr; default;
43+
end;
44+
45+
implementation
46+
47+
uses SysUtils, Math;
48+
49+
constructor TCustomLogger.Create;
50+
begin
51+
inherited;
52+
CaseSensitive := True;
53+
FStringsFilePath := EmptyWideStr;
54+
FCursor := 0;
55+
end;
56+
57+
constructor TCustomLogger.Create(const FilePath: WideString);
58+
begin
59+
Create;
60+
FromFile(FilePath);
61+
FStringsFilePath := FilePath;
62+
FCursor := Max(0, Self.Count-1);
63+
end;
64+
65+
function TCustomLogger.Add(const Item: String): integer;
66+
begin
67+
if Self.IndexOf(Item) < 0 then
68+
FCursor := inherited Add(Item);
69+
Result := FCursor;
70+
end;
71+
72+
function TCustomLogger.Scroll(Direction: TScrollDirection): String;
73+
begin
74+
Result := Self[FCursor];
75+
case Direction of
76+
sdBack: begin
77+
Dec(FCursor);
78+
if FCursor < 0 then FCursor := Self.Count-1;
79+
end;
80+
sdForward: begin
81+
Inc(FCursor);
82+
if FCursor = Self.Count then FCursor := 0;
83+
end;
84+
end;
85+
end;
86+
87+
function TCustomLogger.GetStr(const Index: Integer): String;
88+
begin
89+
Result := EmptyStr;
90+
if (Index > -1) and (Index < Self.Count) then
91+
Result := Self.Strings[Index];
92+
end;
93+
94+
procedure TCustomLogger.ToFile;
95+
var
96+
hFStream: THandleStream;
97+
hDest: THandle;
98+
begin
99+
try
100+
hDest := FileOpen(FStringsFilePath, fmOpenReadWrite or fmShareExclusive);
101+
if hDest = THandle(-1) then
102+
hDest := FileCreate(FStringsFilePath);
103+
try
104+
hFStream := THandleStream.Create(hDest);
105+
inherited SaveToStream(hFStream, False);
106+
finally
107+
FileClose(hFStream.Handle);
108+
hFStream.Free;
109+
end;
110+
except
111+
on E: Exception do
112+
begin
113+
if (not Assigned(hFStream)) and (hDest <> THandle(-1)) then
114+
FileClose(hDest);
115+
end;
116+
end;
117+
end;
118+
119+
procedure TCustomLogger.FromFile(const FilePath: WideString);
120+
var
121+
hFStream: THandleStream;
122+
hSrc: THandle;
123+
begin
124+
try
125+
hSrc := FileOpen(FilePath, fmOpenRead or fmShareDenyWrite);
126+
if hSrc <> THandle(-1) then begin
127+
try
128+
hFStream := THandleStream.Create(hSrc);
129+
inherited LoadFromStream(hFStream, False);
130+
finally
131+
FileClose(hFStream.Handle);
132+
hFStream.Free;
133+
end;
134+
end;
135+
except
136+
on E: Exception do
137+
begin
138+
if (not Assigned(hFStream)) and (hSrc <> THandle(-1)) then
139+
FileClose(hSrc);
140+
end;
141+
end;
142+
end;
143+
144+
end.

Source/Plugin/Src/FSIWrapper.pas

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ interface
5252
{$IFDEF FPC}RichMemo, RichMemoHelpers,{$ENDIF}
5353
// windows pipes wrapper
5454
FpcPipes,
55+
CustomLogger,
5556
// configuration manager
5657
Config;
5758

@@ -68,13 +69,16 @@ TFSIViewer = class
6869
_config: TConfiguration;
6970
_editor: TRichEdit;
7071
_pipedConsole: TPipeConsole;
72+
_logger: TCustomLogger;
7173
_editableAreaStartCoord: TPoint;
7274
_editableAreaStartPos: Integer;
7375
_defEditorWndProc: TWndMethod;
7476
_onSendText: TNotifyEvent;
7577
_onResultOutput: TNotifyEvent;
7678
_onErrorOutput: TNotifyEvent;
77-
private
79+
procedure SetSelection;
80+
procedure TrimSelection;
81+
procedure WritePrompt(ClearFirst: Boolean = False);
7882

7983
/// <summary>
8084
/// Create an instance of FSI and also the pipes needed to interact with it.
@@ -138,6 +142,11 @@ TFSIViewer = class
138142
/// Check for non-empty text selection and enable "Copy" menu item if true
139143
/// </summary>
140144
procedure doOnContextMenuPopup(sender: TObject);
145+
146+
/// <summary>
147+
/// Fill the REPL prompt from the user's input history.
148+
/// </summary>
149+
procedure LoadFromHistory(Direction: TScrollDirection);
141150
public
142151
constructor Create;
143152
destructor Destroy; override;
@@ -175,6 +184,7 @@ TFSIViewer = class
175184
/// Get the instance of the editor that interfaces with FSI.
176185
/// </summary>
177186
property Editor: TRichEdit read _editor;
187+
property Logger: TCustomLogger read _logger;
178188

179189
/// <summary>
180190
/// Event raised when text is sent to FSI.
@@ -217,6 +227,8 @@ destructor TFSIViewer.Destroy;
217227

218228
if Assigned(_pipedConsole) then
219229
FreeAndNil(_pipedConsole);
230+
if Assigned(_logger) then
231+
FreeAndNil(_logger);
220232
if Assigned(_editor) then
221233
FreeAndNil(_editor);
222234
if Assigned(_config) then
@@ -293,6 +305,7 @@ procedure TFSIViewer.SendText(const selText: WideString; addDelimiter, appendEdi
293305
if (appendEditor) then
294306
AddToEditor(finalText);
295307
_pipedConsole.Write(finalText[1], SizeOf(Char) * Length(finalText));
308+
_logger.Add(Trim(finalText));
296309
end;
297310

298311
if Assigned(_onSendText) then
@@ -340,6 +353,7 @@ procedure TFSIViewer.createFSI;
340353
_pipedConsole := TPipeConsole.Create(Nil);
341354
_pipedConsole.OnOutput := doOnPipeOutput;
342355
_pipedConsole.OnError := doOnPipeError;
356+
_logger := TCustomLogger.Create(IncludeTrailingPathDelimiter(Npp.GetPluginConfigDirectory) + 'fsi_history.txt');
343357
end;
344358

345359
procedure TFSIViewer.createContextMenu;
@@ -462,15 +476,14 @@ procedure TFSIViewer.doOnPipeError(sender: TObject; stream: TStream);
462476
finally
463477
strStream.Free;
464478
end;
465-
AddToEditor('> ');
466-
updateEditableAreaStart;
479+
WritePrompt;
467480
end;
468481

469482
procedure TFSIViewer.doOnEditorKeyDown(sender: TObject; var Key: Word;
470483
Shift: TShiftState);
471484
var
472485
newText: WideString;
473-
IsCtrl, IsShift: Boolean;
486+
IsCtrl, IsShift, DelimiterNeeded: Boolean;
474487
begin
475488
if (not isConsoleRunning) then
476489
Exit;
@@ -485,21 +498,33 @@ procedure TFSIViewer.doOnEditorKeyDown(sender: TObject; var Key: Word;
485498
end
486499
else if (isEditorCaretInAValidPos) then
487500
begin
501+
TrimSelection;
488502
if (Key = VK_RETURN) then
489503
begin
490-
_editor.SelStart := _editableAreaStartPos;
491-
_editor.SelLength := _editor.GetTextLen - _editableAreaStartPos;
504+
SetSelection;
492505
newText := {$IFDEF FPC}UTF8Decode{$ENDIF}(_editor.SelText);
493506
updateEditableAreaStart;
494507
if WideSameText(Copy(newText, 0, 2), '> ') then
495508
newText := Copy(newText, 2);
496509
newText := newText + #13#10;
497-
SendText(newText, false, false);
510+
if WideSameText(newText, #13#10) then
511+
// send a simple RETURN for multi-line input or paging
512+
DelimiterNeeded := True
513+
else
514+
DelimiterNeeded := False;
515+
SendText(newText, DelimiterNeeded, false);
498516
end
499-
else if (Key = VK_UP) then
517+
else if (Key in [VK_UP, VK_DOWN]) then
500518
begin
501-
if (_editor.CaretPos.Y = _editableAreaStartCoord.Y) then
519+
if (_editor.CaretPos.Y >= _editableAreaStartCoord.Y) then
520+
begin
521+
if (_editor.SelLength = 0) then
522+
case Key of
523+
VK_UP: LoadFromHistory(sdBack);
524+
VK_DOWN: LoadFromHistory(sdForward);
525+
end;
502526
Key := 0;
527+
end;
503528
end
504529
else if (Key = VK_LEFT) then
505530
begin
@@ -536,7 +561,9 @@ procedure TFSIViewer.doOnEditorKeyDown(sender: TObject; var Key: Word;
536561
begin
537562
if Key in [VK_INSERT, VK_DELETE] then
538563
MessageBeep(MB_ICONWARNING);
539-
end;
564+
end // do not restrict caret movement via arrow keys
565+
else if (Key in [VK_LEFT, VK_UP, VK_RIGHT, VK_DOWN]) then
566+
Exit;
540567
Key := 0;
541568
end;
542569
end;
@@ -548,18 +575,18 @@ procedure TFSIViewer.doOnEditorChange(Sender: TObject);
548575

549576
procedure TFSIViewer.doOnEditorClearContextMenuClick(sender: TObject);
550577
begin
551-
_editor.Clear;
552-
AddToEditor('> ');
553-
updateEditableAreaStart;
578+
WritePrompt(True);
554579
end;
555580

556581
procedure TFSIViewer.doOnEditorCutContextMenuClick(Sender: TObject);
557582
begin
583+
TrimSelection;
558584
_editor.CutToClipboard;
559585
end;
560586

561587
procedure TFSIViewer.doOnEditorCopyContextMenuClick(sender: TObject);
562588
begin
589+
TrimSelection;
563590
_editor.CopyToClipboard;
564591
updateEditableAreaStart;
565592
end;
@@ -588,4 +615,32 @@ procedure TFSIViewer.doOnContextMenuPopup(sender: TObject);
588615

589616
{$ENDREGION}
590617

618+
procedure TFSIViewer.LoadFromHistory(Direction: TScrollDirection);
619+
begin
620+
SetSelection;
621+
_editor.ClearSelection;
622+
AddToEditor(_logger.Scroll(Direction));
623+
_editor.SelStart := _editor.GetTextLen;
624+
_editor.SelLength := 0;
625+
end;
626+
627+
procedure TFSIViewer.WritePrompt(ClearFirst: Boolean);
628+
begin
629+
if ClearFirst then _editor.Clear;
630+
AddToEditor('> ');
631+
updateEditableAreaStart;
632+
end;
633+
634+
procedure TFSIViewer.TrimSelection;
635+
begin
636+
if SameStr(Copy(_editor.SelText, 0, 2), '> ') and (_editor.SelStart < _editableAreaStartPos) then
637+
SetSelection;
638+
end;
639+
640+
procedure TFSIViewer.SetSelection;
641+
begin
642+
_editor.SelStart := _editableAreaStartPos;
643+
_editor.SelLength := _editor.GetTextLen - _editableAreaStartPos;
644+
end;
645+
591646
end.

0 commit comments

Comments
 (0)