{
  TIGCC IDE

  Copyright (C) 2004 Fréderic Bour
  Copyright (C) 2004 Sebastian Reichelt
  Copyright (C) 2006-2007 Kevin Kofler

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2, or (at your option)
  any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software Foundation,
  Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA.
}

unit CompletionForm;

interface

uses
	Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, MasterUnit,
	Dialogs, StdCtrls, ComCtrls, CodeCompletion, SourceFileUnit, UtilsDos, MemoComponentUnit,
	HtFormatting, Menus, ActnList;

const
	SymbolFileNotFound = 'Symbol %s is declared in %s, but file can''t be found!';
	AddTemplatesMsg = 'To add templates, go to Preferences->Coding';

type
	TCompForm = class(TForm)
		CompList: TListBox;
		CompStatus: TStatusBar;
		procedure FormCreate(Sender: TObject);
		procedure FormDestroy(Sender: TObject);
		procedure FormDeactivate(Sender: TObject);
		procedure CompListDrawItem(Control: TWinControl; Index: Integer;
			Rect: TRect; State: TOwnerDrawState);
		procedure CompListDblClick(Sender: TObject);
		procedure CompListKeyDown(Sender: TObject; var Key: Word;
			Shift: TShiftState);
		procedure CompListKeyUp(Sender: TObject; var Key: Word;
			Shift: TShiftState);
		procedure CompListKeyPress(Sender: TObject; var Key: Char);
	private
		procedure SetEditor(const Value: TMemoComponent);
	protected
		{ Déclarations protégées }
		C: TCompletionList;
		CompRow: Integer;
		StopKey: Boolean;
		HintWindow: THtHintWindow;

		FSourceFile: TSourceFile;
		FEditor: TMemoComponent;

		procedure CreateParams(var Params: TCreateParams); override;
		function GetFileData(const F: string): string;
		procedure NeedFile(const FileName: string; out CCFData: string);

	protected
		// Be informed when active control change :)

		OldActiveControlChange: TNotifyEvent;
		procedure ActiveControlChange(Sender: TObject);
	public
		{ Déclarations publiques }

		// Get & Replace word under caret
		procedure DelimitateWord(const Line: string; var StartPos: Integer; out EndPos: Integer);
		function GetWord(Full, RemoveSpace: Boolean): string;
		procedure SetWord(const Value: string);

		// Show Completion Windows
		property List: TCompletionList read C;
		procedure RebuildList;
		procedure ShowCompletion;
		procedure CloseCompletion;
		procedure ApplyCompletion;

		// Show an hint window about the symbol Symbol, or symbol under caret if ''
		function ShowSymbolInfo(Symbol: string = ''): Boolean;
		procedure CloseSymbolInfo;

		// Find symbol declaration
		procedure FindSymbolDecl;

	protected
		OldChange: TNotifyEvent;
		OldKeyDown: TKeyEvent;
		OldKeyPress: TKeyPressEvent;

		// Redirect Event from Editor
		procedure Editor_Enter(Sender: TMemoComponent);
		procedure Editor_Exit(Sender: TObject);
		procedure Editor_KeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
		procedure Editor_KeyPress(Sender: TObject; var Key: Char);
		procedure Editor_Change(Sender: TObject);

	public
		property Editor: TMemoComponent read FEditor write SetEditor;
	end;

var
	CompForm: TCompForm;

	// TIGCC integration
function SourceFileFromEditor(M: TMemoComponent): TSourceFile;

procedure InsertAction(Owner: TComponent; After: TMenuItem; Action: TBasicAction);
procedure InsertsAction(Owner: TComponent; const Afters: array of TMenuItem; Action: TBasicAction);

implementation

uses Types, StrUtils, Math, TmpltForm, MainUnit;

{$R *.dfm}

function SourceFileFromEditor(M: TMemoComponent): TSourceFile;
var
	i: Integer;
	Lst: TSourceFiles;
	Src: TSourceFile;
begin
	Lst := MainForm.SourceFiles;
	for i := 0 to Lst.Count - 1 do
	begin
		Src := TSourceFile(Lst.Items[i]);
		if Src.Editor = M then
		begin
			Result := Src;
			Exit;
		end;
	end;
	Result := nil;
end;

procedure InsertAction(Owner: TComponent; After: TMenuItem; Action: TBasicAction);
var
	Item: TMenuItem;
begin
	Item := TMenuItem.Create(Owner);
	Item.Action := Action;
	with After.Parent do
		Insert(IndexOf(After) + 1, Item);
end;

procedure InsertsAction(Owner: TComponent; const Afters: array of TMenuItem; Action: TBasicAction);
var
	i: Integer;
begin
	for i := Low(Afters) to High(Afters) do
		InsertAction(Owner, Afters[i], Action);
end;

// GUI Methods

procedure TCompForm.FormCreate(Sender: TObject);
begin
	C := TCompletionList.Create;
	C.OnNeedFile := NeedFile;
	HintWindow := THtHintWindow.Create(Self);
	OldActiveControlChange := Screen.OnActiveControlChange;
	Screen.OnActiveControlChange := ActiveControlChange;
end;

procedure TCompForm.FormDestroy(Sender: TObject);
begin
	if Assigned(Screen.OnActiveControlChange) then
		Screen.OnActiveControlChange := OldActiveControlChange;
	HintWindow.Free;
	C.Free;
end;

procedure TCompForm.FormDeactivate(Sender: TObject);
var
	M: TMemoComponent;
begin
	M := Editor;
	if (M = nil) or (not M.Focused) then
		CloseCompletion;
end;

procedure TCompForm.CreateParams(var Params: TCreateParams);
begin
	inherited;
	Params.Style := WS_POPUP or WS_SYSMENU or WS_THICKFRAME;
end;

procedure TCompForm.CompListDrawItem(Control: TWinControl; Index: Integer;
	Rect: TRect; State: TOwnerDrawState);
const
	MinWidth = 62;
var
	C: TCompletionItem;
	S: string;
	Cnv: TCanvas;
	Selected: Boolean;
	W: Integer;
begin
	if (Index >= 0) and (Index < List.Count) then
	begin
		Cnv := CompList.Canvas;
		Selected := odSelected in State;
		C := List.Completion[Index];
		S := List[Index];
		if Assigned(C) then
		begin
			Cnv.Font.Style := [];
			DrawHtText(C.Left, Rect, Cnv, W, Selected);
			Inc(Rect.Left, W);
		end;
		if Rect.Left < MinWidth then
			Rect.Left := MinWidth;
		Inc(Rect.Left, 2);
		Cnv.Font.Style := [fsBold];
		DrawHtText(S, Rect, Cnv, W, Selected);
		Inc(Rect.Left, W + 2);
		if Assigned(C) then
		begin
			Cnv.Font.Style := [];
			DrawHtText(C.Right, Rect, CompList.Canvas, Rect.Left, Selected);
			if Selected then
				CompStatus.SimpleText := C.Description;
		end;
	end;
end;

procedure TCompForm.CompListDblClick(Sender: TObject);
begin
	ApplyCompletion;
end;

// Completion Tools

procedure TCompForm.RebuildList;
var
	S: TSourceFile;
begin
	List.Clear;
	S := SourceFileFromEditor(Editor);
	if Assigned(S) then
	begin
		if S is TCSourceFile then
			List.AddCompletion(MakeCCF_C(ExtractFileName(S.FileName), TCSourceFile(S)))
		else if S is THeaderSourceFile then
			List.AddCompletion(MakeCCF_H(ExtractFileName(S.FileName), THeaderSourceFile(S).SourceEditor.Lines));
	end;
	CompList.Count := List.Count;
end;

procedure TCompForm.ShowCompletion;
var
	M: TMemoComponent;
	P: TPoint;
	i: Integer;
begin
	M := Editor;
	CloseSymbolInfo;
	if Assigned(M) then
	begin
		RebuildList;
		CompRow := M.Selection.StartRowCol.Row;
		P := M.Selection.StartPoint;
		P := M.ClientToScreen(P);
		Left := P.X + 4;
		Top := P.Y + 20;
		List.Find(GetWord(True, False), i);
		CompList.ItemIndex := i;
		LockWindowUpdate(M.ParentWindow);
		Show;
		M.SetFocus;
		LockWindowUpdate(0);
	end;
end;

procedure TCompForm.CloseCompletion;
begin
	Hide;
end;

function TCompForm.ShowSymbolInfo(Symbol: string = ''): Boolean;
	function SimilarityDistance(const A, B: string): Integer;
	var
		i, l: Integer;
	begin
		l := Min(Length(A), Length(B));
		i := 1;
		while (i <= l) and (UpCase(A[i]) = UpCase(B[i])) do
			Inc(i);
		Result := l + 1 - i;
	end;
	function Similar(const A, B: string): Boolean;
	var
		l: Integer;
	begin
		l := Min(Length(A), Length(B));
		Result := SimilarityDistance(A, B) <= (l shr 1);
	end;
var
	M: TMemoComponent;
	P: TPoint;
	R: TRect;
	i: Integer;
	C: TCompletionItem;
	T: string;
begin
	CloseCompletion;
	Result := False;
	if Symbol = '' then
		Symbol := GetWord(False, True);
	if Symbol = '' then
	begin
		CloseSymbolInfo;
		Exit;
	end;

	M := Editor;
	if Assigned(M) then
	begin
		RebuildList;

		// Find Symbol
		if List.Find(Symbol, i) then
			T := ''
		else if (List.Count > 0) and (i >= 0) and (i <= List.Count) then
		begin
			if i = 0 then
			begin
				if Similar(Symbol, List[i]) then
					T := List[i] + ' ?'#13#10
				else
					Exit;
			end
			else if i = List.Count then
			begin
				if Similar(Symbol, List[i - 1]) then
					T := List[i - 1] + ' ?'#13#10
				else
					Exit;
			end
			else
			begin
				if SimilarityDistance(Symbol, List[i - 1]) < SimilarityDistance(Symbol, List[i]) then
					Dec(i);
				if Similar(Symbol, List[i]) then
					T := List[i] + ' ?'#13#10
				else
					Exit;
			end;
		end
		else
			Exit;

		// Show Symbol info
		C := List.Completion[i];
		if Assigned(C) then
		begin
			T := T + C.Left + ' ' + C.Right + #13#10 + C.Description;
			// Calc hint window position
			CompRow := M.Selection.StartRowCol.Row;
			P := M.ClientToScreen(M.Selection.StartPoint);
			R := HintWindow.CalcHintRect(Screen.Width, T, nil);
			Inc(R.Left, P.X + 4);
			Inc(R.Right, P.X + 4);
			Inc(R.Top, P.Y + 20);
			Inc(R.Bottom, P.Y + 20);
			LockWindowUpdate(M.ParentWindow);
			HintWindow.ActivateHint(R, T);
			Result := True;
			M.SetFocus;

			// Bug with TSourceFileForm ??!!
			if Assigned(M.Parent) and (M.Parent is TForm) then
				M.Parent.BringToFront;
			LockWindowUpdate(0);
		end;
	end;
end;

procedure TCompForm.CloseSymbolInfo;
begin
	if IsWindowVisible(HintWindow.Handle) then
		ShowWindow(HintWindow.Handle, SW_HIDE);
end;

procedure TCompForm.ApplyCompletion;
var
	i: Integer;
begin
	i := CompList.ItemIndex;
	if (i >= 0) and (i < List.Count) then
	begin
		SetWord(List[i]);
		CloseCompletion;
	end;
end;

// Find Files

function TCompForm.GetFileData(const F: string): string;
var
	S: TSourceFile;
	Folder: string;
begin
	S := MainForm.SourceFiles.FindFileNameOnly(F);
	Folder := WithBackslash(TIGCCFolder);
	if Assigned(S) and (S is TSourceTextSourceFile) then
		Result := TSourceTextSourceFile(S).Content
	else if FileExists(ExpandFileName(F)) then
		Result := FileToString(ExpandFileName(F))
	else if FileExists(Folder + CIncludeLocation + F) then
		Result := FileToString(Folder + CIncludeLocation + F)
	else if FileExists(Folder + ASMIncludeLocation + F) then
		Result := FileToString(Folder + ASMIncludeLocation + F)
	else
		Result := '';
end;

procedure TCompForm.NeedFile(const FileName: string; out CCFData: string);
var
	CCFName: string;
	Lst: TStringList;
begin
	// Search CCF file
	CCFName := WithBackslash(TIGCCFolder) + CompletionLocation + ChangeFileExt(FileName, '.ccf');
	if FileExists(CCFName) then
		CCFData := FileToString(CCFName)
	else
	begin
		// No CCF ? Generate info from header...
		if UpperCase(ExtractFileExt(FileName)) = '.H' then
		begin
			Lst := TStringList.Create;
			Lst.Text := GetFileData(FileName);
			if Lst.Count > 0 then
				CCFData := MakeCCF_H(FileName, Lst);
			Lst.Free;
		end;
	end;
end;

// Word manipulations

procedure TCompForm.DelimitateWord(const Line: string; var StartPos: Integer; out EndPos: Integer);
const
	ValidChars = ['A'..'Z', 'a'..'z', '0'..'9', '_', '$', '#'];
var
	i, j, l: Integer;
begin
	i := StartPos - 1;
	j := StartPos - 1;
	l := Length(Line);
	// Find start pos
	while (i > 0) and (i <= l) and (Line[i] in ValidChars) do
		Dec(i);
	// Find end pos
	while (j > 0) and (j <= l) and (Line[j] in ValidChars) do
		Inc(j);
	if j = StartPos - 1 then
	begin
		j := StartPos;
		while (j > 0) and (j <= l) and (Line[j] in ValidChars) do
			Inc(j);
	end;
	Dec(j);
	Inc(i);
	StartPos := i;
	EndPos := j;
end;

function TCompForm.GetWord(Full, RemoveSpace: Boolean): string;
var
	M: TMemoComponent;
	S: string;
	T: TTextCell;
	StPos, EndPos, l: Integer;
begin
	M := Editor;
	Result := '';
	if Assigned(M) then
	begin
		T := M.Selection.StartRowCol;
		if (T.Row > 0) and (T.Row <= M.LineCount) then
		begin
			S := M.Lines[T.Row - 1];
			StPos := T.Col;
			l := Length(S);
			if RemoveSpace and (StPos > 1) and (StPos <= l) then
			begin
				if S[StPos - 1] = ' ' then
					Dec(StPos);
				while (StPos > 1) and (S[StPos] in ['(', ' ']) do
					Dec(StPos);
			end;
			DelimitateWord(S, StPos, EndPos);
			if Full then
				Result := Trim(Copy(S, StPos, EndPos - StPos + 1))
			else
				Result := Trim(Copy(S, StPos, T.Col - StPos));
		end;
	end;
end;

procedure TCompForm.SetWord(const Value: string);
var
	M: TMemoComponent;
begin
	M := Editor;
	if Assigned(M) then
	begin
		with M.Selection do
		begin
			SelectWord;
			Text := Value;
		end;
	end;
end;

// Editor Events

procedure TCompForm.Editor_KeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
	procedure Select(Direction, Page: Boolean);
	var
		HowMany, i, c: Integer;
	begin
		i := CompList.ItemIndex;
		c := List.Count;
		if Page then
			HowMany := CompList.Height div CompList.ItemHeight
		else
			HowMany := 1;
		if Direction then
			Inc(i, HowMany)
		else
			Dec(i, HowMany);
		if i <= 0 then
			CompList.ItemIndex := 0
		else if i >= c then
			CompList.ItemIndex := c - 1
		else
			CompList.ItemIndex := i;
	end;
var
	NewKey: Word;
begin
	if Assigned(OldKeyDown) then
		OldKeyDown(Sender, Key, Shift);
	if Visible then
	begin
		NewKey := 0;
		case Key of
			VK_ESCAPE: Close;
			VK_RETURN:
				begin
					ApplyCompletion;
					StopKey := True;
				end;
			VK_UP: Select(False, False);
			VK_DOWN: Select(True, False);
			VK_PRIOR: Select(False, True);
			VK_NEXT: Select(True, True);
			VK_HOME: CompList.ItemIndex := 0;
			VK_END: CompList.ItemIndex := List.Count - 1;
			VK_LEFT, VK_RIGHT:
				begin
					Editor_Change(Sender);
					NewKey := Key;
				end;
		else
			NewKey := Key;
		end;
		Key := NewKey;
	end
	else if (Shift = [ssCtrl]) and ((Key = VK_SPACE) or (Key = Ord('M'))) then
	begin
		Key := 0;
		StopKey := True;
		ShowCompletion;
	end
	else if (Shift = [ssCtrl]) and (Key = Ord('J')) then
	begin
		Key := 0;
		StopKey := True;
		if TemplateForm.Templates.Count > 0 then
			TemplateForm.Show
		else
			ShowMessage(AddTemplatesMsg);
	end
	else if (Key in [VK_BACK, VK_ESCAPE]) then // or not (Key in [Ord('A')..Ord('Z'), Ord('0')..Ord('9')
		CloseSymbolInfo;
end;

procedure TCompForm.Editor_KeyPress(Sender: TObject; var Key: Char);
begin
	if Assigned(OldKeyPress) then
		OldKeyPress(Sender, Key);
	if StopKey then
	begin
		Key := #0;
		StopKey := False;
	end
	else
	begin
		case Key of
			'(': if Visible or not IsWindowVisible(HintWindow.Handle) then ShowSymbolInfo;
			')': CloseSymbolInfo;
		end;
	end;
end;

procedure TCompForm.Editor_Change(Sender: TObject);
var
	i: Integer;
	M: TMemoComponent;
begin
	if Assigned(OldChange) then
		OldChange(Sender);
	M := Editor;
	if Visible then
	begin
		if Assigned(M) then
		begin
			if CompRow <> M.Selection.StartRowCol.Row then
				CloseCompletion
			else
			begin
				List.Find(GetWord(False, False), i);
				CompList.ItemIndex := i;
			end;
		end
		else
			CloseCompletion;
	end
	else if Assigned(M) and (CompRow <> M.Selection.StartRowCol.Row) then
		CloseSymbolInfo;
end;

procedure TCompForm.Editor_Enter(Sender: TMemoComponent);
begin
	if Assigned(Sender) then
	begin
		FEditor := Sender;

		CloseCompletion;
		CloseSymbolInfo;

		OldKeyDown := Editor.OnKeyDown;
		OldKeyPress := Editor.OnKeyPress;
		OldChange := Editor.OnChange;

		Editor.OnKeyDown := Editor_KeyDown;
		Editor.OnKeyPress := Editor_KeyPress;
		Editor.OnChange := Editor_Change;
		Editor.OnSelectionChange := Editor_Change;
	end
	else
	begin
		FEditor := nil;
		OldKeyDown := nil;
		OldKeyPress := nil;
		OldChange := nil;
	end;
end;

procedure TCompForm.Editor_Exit(Sender: TObject);
begin
	if Assigned(Editor) then
	begin
		Editor.OnKeyDown := OldKeyDown;
		Editor.OnKeyPress := OldKeyPress;
		Editor.OnChange := OldChange;
		Editor.OnSelectionChange := OldChange;
	end;
	CloseSymbolInfo;
	if not Focused then
	begin
		FEditor := nil;
		CloseCompletion;
	end;
end;

procedure TCompForm.CompListKeyDown(Sender: TObject; var Key: Word;
	Shift: TShiftState);
var
	M: TMemoComponent;
begin
	M := Editor;
	if Assigned(M) then
	begin
		M.HandleKeyDown(Key, Shift);
		M.SetFocus;
		Key := 0;
	end
	else
		CloseCompletion;
end;

procedure TCompForm.CompListKeyUp(Sender: TObject; var Key: Word;
	Shift: TShiftState);
var
	M: TMemoComponent;
begin
	M := Editor;
	if Assigned(M) then
	begin
		M.HandleKeyUp(Key, Shift);
		M.SetFocus;
		Key := 0;
	end
	else
		CloseCompletion;
end;

procedure TCompForm.CompListKeyPress(Sender: TObject; var Key: Char);
var
	M: TMemoComponent;
begin
	M := Editor;
	if Assigned(M) then
	begin
		M.HandleKeyPress(Key);
		M.SetFocus;
	end;
end;

procedure TCompForm.FindSymbolDecl;
var
	i, p: Integer;
	Symbol, S: string;
	C: TCompletionItem;
	SourceFile: TSourceFile;
begin
	RebuildList;
	Symbol := GetWord(True, True);
	if (Symbol <> '') and List.Find(Symbol, i) then
	begin
		C := List.Completion[i];
		if Assigned(C) and (C.SourceFileName <> '') then
		begin
			SourceFile := MainForm.SourceFiles.FindFileNameOnly(C.SourceFileName);
			if SourceFile = nil then
			begin
				if FileExists(ExpandFileName(C.SourceFileName)) then
					SourceFile := MainForm.AddSourceFile(ExpandFileName(C.SourceFileName), True)
				else if FileExists(WithBackslash(TIGCCFolder) + CIncludeLocation + C.SourceFileName) then
					SourceFile := MainForm.AddSourceFile(WithBackslash(TIGCCFolder) + CIncludeLocation + C.SourceFileName, True)
				else if FileExists(WithBackslash(TIGCCFolder) + ASMIncludeLocation + C.SourceFileName) then
					SourceFile := MainForm.AddSourceFile(WithBackslash(TIGCCFolder) + ASMIncludeLocation + C.SourceFileName, True)
			end;
			if Assigned(SourceFile) and (SourceFile is TTextSourceFile) then
				with TTextSourceFile(SourceFile).TextEditor do
				begin
					if (C.Line < Cardinal(Lines.Count)) then
					begin
						S := Lines[C.Line];
						p := Pos(Symbol, S);
						if p = 0 then
							p := 1;
					end
					else
						p := 1;
					Selection.StartRowCol := TextCell(C.Line + 1, p);
					Selection.RLength := 0;
					Selection.ScrollInView(4);
				end
			else
				ShowMessageFmt(SymbolFileNotFound, [Symbol, C.SourceFileName]);
		end;
	end;
end;

procedure TCompForm.ActiveControlChange(Sender: TObject);
var
	W: TWinControl;
begin
	W := Screen.ActiveControl;
	if (W = Editor) or (W = Self) or (W = CompList) or (W = TemplateForm) or (W = TemplateForm.TmpltList) then
		Exit;
	if Assigned(W) and (W is TMemoComponent) then
		Editor := TMemoComponent(W)
	else
		Editor := nil;
	if Assigned(OldActiveControlChange) then
		OldActiveControlChange(Sender);
end;

procedure TCompForm.SetEditor(const Value: TMemoComponent);
begin
	if Value <> FEditor then
	begin
		Editor_Exit(Self);
		Editor_Enter(Value);
		FEditor := Value;
	end;
end;

end.

